《CMake實踐》筆記三:構建靜態庫(.a) 與 動態庫(.so) 及 如何使用外部共享庫和頭文件


《CMake實踐》筆記一:PROJECT/MESSAGE/ADD_EXECUTABLE

《CMake實踐》筆記二:INSTALL/CMAKE_INSTALL_PREFIX

《CMake實踐》筆記三:構建靜態庫與動態庫 及 如何使用外部共享庫和頭文件

 

五、靜態庫與動態庫構建

讀者雲,太能羅唆了,一個Hello World就折騰了兩個大節。OK,從本節開始,我們不再折騰Hello World了,我們來折騰Hello World的共享庫。

本節的任務:

1、建立一個靜態庫和動態庫,提供HelloFunc函數供其他程序編程使用,HelloFunc向終端輸出Hello World字符串。

2、安裝頭文件與共享庫。

 

(一)、准備工作:

在/backup/cmake目錄建立t3目錄,用於存放本節涉及到的工程

 

(二)、建立共享庫

cd /backup/cmake/t3
mkdir lib

在t3目錄下建立CMakeLists.txt,內容如下:

PROJECT(HELLOLIB)
ADD_SUBDIRECTORY(lib)

在lib目錄下建立兩個源文件hello.c與hello.h

hello.c內容如下:

#include "hello.h"
void HelloFunc()
{
    printf("Hello World\n");
}

hello.h內容如下:

#ifndef HELLO_H
#define HELLO_H
#include <stdio.h>
void HelloFunc();
#endif

在lib目錄下建立CMakeLists.txt,內容如下:

SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

 

(三)、編譯共享庫

仍然采用 out-of-source 編譯的方式,按照習慣,我們建立一個build目錄,在build目錄中

cmake ..
make

這時,你就可以在lib目錄得到一個libhello.so,這就是我們期望的共享庫。

如果你要指定libhello.so生成的位置,可以通過在主工程文件CMakeLists.txt中修改ADD_SUBDIRECTORY(lib)指令來指定一個編譯輸出位置或者在lib/CMakeLists.txt中添加SET(LIBRARY_OUTPUT_PATH <路徑>)來指定一個新的位置。這兩者的區別我們上一節已經提到了,所以,這里不再贅述,下面,我們解釋一下一個新的指令ADD_LIBRARY:

ADD_LIBRARY(libname [SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL]source1 source2 ... sourceN)

你不需要寫全libhello.so,只需要填寫hello即可,cmake系統會自動為你生成libhello.X。類型有三種:

  • SHARED,動態庫(擴展名為.so)
  • STATIC,靜態庫(擴展名為.a)
  • MODULE,在使用dyld的系統有效,如果不支持dyld,則被當作SHARED對待。
  • EXCLUDE_FROM_ALL 參數的意思是這個庫不會被默認構建,除非有其他的組件依賴或者手工構建。

C/C++ 靜態鏈接庫(.a) 與 動態鏈接庫(.so)庫

Linux C 靜態庫(.a) 與 動態庫(.so)的詳解

 

(四)、添加靜態庫

同樣使用上面的指令,我們在支持動態庫的基礎上再為工程添加一個靜態庫,按照一般的習慣,靜態庫名字跟動態庫名字應該是一致的,只不過后綴是.a罷了。下面我們用這個指令再來添加靜態庫:

ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})

然后再在build目錄進行外部編譯,我們會發現,靜態庫根本沒有被構建,仍然只生成了一個動態庫。因為hello作為一個target是不能重名的,所以,靜態庫構建指令無效。

如果我們把上面的hello修改為hello_static:

ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

就可以構建一個libhello_static.a的靜態庫了。這種結果顯示不是我們想要的,我們需要的是名字相同的靜態庫和動態庫,因為target名稱是唯一的,所以,我們肯定不能通過ADD_LIBRARY指令來實現了。這時候我們需要用到另外一個指令:

SET_TARGET_PROPERTIES,其基本語法是:
SET_TARGET_PROPERTIES(target1 target2 ...PROPERTIES prop1 value1prop2 value2 ...)

這條指令可以用來設置輸出的名稱,對於動態庫,還可以用來指定動態庫版本和API版本。在本例中,我們需要作的是向lib/CMakeLists.txt中添加一條:

SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")

這樣,我們就可以同時得到libhello.so/libhello.a兩個庫了。與他對應的指令是:

GET_TARGET_PROPERTY(VAR target property)

具體用法如下例,我們向lib/CMakeListst.txt中添加:

GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME)
MESSAGE(STATUS  "This is the hello_static OUTPUT_NAME:" ${OUTPUT_VALUE})

如果沒有這個屬性定義,則返回 NOTFOUND。

讓我們來檢查一下最終的構建結果,我們發現,libhello.a已經構建完成,位於build/lib目錄中,但是libhello.so去消失了。這個問題的原因是:cmake在構建一個新的target時,會嘗試清理掉其他使用這個名字的庫,因為,在構建libhello.a時,就會清理掉libhello.so.為了回避這個問題,比如再次使用SET_TARGET_PROPERTIES定義

CLEAN_DIRECT_OUTPUT屬性。向lib/CMakeLists.txt中添加:

SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

這時候,我們再次進行構建,會發現build/lib目錄中同時生成了 libhello.solibhello.a

 

(五)、動態庫版本號

按照規則,動態庫是應該包含一個版本號的,我們可以看一下系統的動態庫,一般情況是

libhello.so.1.2
libhello.so ->libhello.so.1
libhello.so.1->libhello.so.1.2

為了實現動態庫版本號,我們仍然需要使用SET_TARGET_PROPERTIES指令。具體使用方法如下:

SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)

VERSION指代動態庫版本,SOVERSION指代API版本。將上述指令加入lib/CMakeLists.txt中,重新構建看看結果。

在build/lib目錄會生成:

libhello.so.1.2
libhello.so.1->libhello.so.1.2
libhello.so ->libhello.so.1

 

(六)、安裝共享庫和頭文件

以上面的例子,我們需要將libhello.a, libhello.so.x以及hello.h安裝到系統目錄,才能真正讓其他人開發使用,在本例中我們將hello的共享庫安裝到<prefix>/lib目錄,將hello.h安裝到<prefix>/include/hello目錄。

利用上一節了解到的INSTALL指令,我們向lib/CMakeLists.txt中添加如下指令:

INSTALL(TARGETS hello hello_static
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
INSTALL(FILES hello.h DESTINATION include/hello)

注意,靜態庫要使用ARCHIVE關鍵字通過:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
make install

我們就可以將頭文件和共享庫安裝到系統目錄/usr/lib和/usr/include/hello中了。

 

(七)、小結

本小節,我們談到了:

如何通過 ADD_LIBRARY 指令構建動態庫和靜態庫。

如何通過 SET_TARGET_PROPERTIES 同時構建同名的動態庫和靜態庫。

如何通過 SET_TARGET_PROPERTIES 控制動態庫版本

最終使用上一節談到的 INSTALL 指令來安裝頭文件和動態、靜態庫。

在下一節,我們需要編寫另一個高級一點的Hello World來演示怎么使用我們已經構建的構建的共享庫libhello和外部頭文件。

 


 

以下是我跟着做的截圖,確實很方便:(這里有點失誤,設置了個CC的環境變量,所以這里找的是arm-linux-gcc去了。。本意是用gcc編譯的)

 


 

六、如何使用外部共享庫和頭文件

抱歉,本節仍然繼續折騰Hello World。

上一節我們已經完成了libhello動態庫的構建以及安裝,本節我們的任務很簡單:

編寫一個程序使用我們上一節構建的共享庫。

1、准備工作:

請在/backup/cmake目錄建立t4目錄,本節所有資源將存儲在t4目錄。

 

2、重復以前的步驟,建立src目錄,編寫源文件main.c,內容如下:

#include <hello.h>
int main()
{
    HelloFunc();
    return 0;
}

編寫工程主文件CMakeLists.txt

PROJECT(NEWHELLO)
ADD_SUBDIRECTORY(src)

編寫src/CMakeLists.txt

ADD_EXECUTABLE(main main.c)

上述工作已經嚴格按照我們前面季節提到的內容完成了。

 

3、外部構建

按照習慣,仍然建立build目錄,使用cmake ..方式構建。

過程:

cmake ..
make

構建失敗,如果需要查看細節,可以使用第一節提到的方法

make VERBOSE=1

來構建

錯誤輸出為是:

/backup/cmake/t4/src/main.c:1:19: error: hello.h: 沒有那個文件或目錄

 

4、引入頭文件搜索路徑

hello.h位於/usr/include/hello目錄中,並沒有位於系統標准的頭文件路徑,(有人會說了,白痴啊,你就不會include <hello/hello.h>,同志,要這么干,我這一節就沒什么可寫了,只能選擇一個glib或者libX11來寫了,這些代碼寫出來很多同志是看不懂的)

為了讓我們的工程能夠找到hello.h頭文件,我們需要引入一個新的指令

INCLUDE_DIRECTORIES,其完整語法為:

INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)

這條指令可以用來向工程添加多個特定的頭文件搜索路徑,路徑之間用空格分割,如果路徑中包含了空格,可以使用雙引號將它括起來,默認的行為是追加到當前的頭文件搜索路徑的后面,你可以通過兩種方式來進行控制搜索路徑添加的方式:

(1)、CMAKE_INCLUDE_DIRECTORIES_BEFORE,通過SET這個cmake變量為on,可以將添加的頭文件搜索路徑放在已有路徑的前面。

(2)、通過AFTER或者BEFORE參數,也可以控制是追加還是置前。現在我們在src/CMakeLists.txt中添加一個頭文件搜索路徑,方式很簡單,加入:INCLUDE_DIRECTORIES(/usr/include/hello)

進入build目錄,重新進行構建,這是找不到hello.h的錯誤已經消失,但是出現了一個新的錯誤:

main.c:(.text+0x12): undefined reference to `HelloFunc'

因為我們並沒有link到共享庫libhello上。

 

5、為target添加共享庫

我們現在需要完成的任務是將目標文件鏈接到libhello,這里我們需要引入兩個新的指令

LINK_DIRECTORIES和TARGET_LINK_LIBRARIES
LINK_DIRECTORIES的全部語法是:
LINK_DIRECTORIES(directory1 directory2 ...)

這個指令非常簡單,添加非標准的共享庫搜索路徑,比如,在工程內部同時存在共享庫和可執行二進制,在編譯時就需要指定一下這些共享庫的路徑。這個例子中我們沒有用到這個指令。

TARGET_LINK_LIBRARIES的全部語法是:
TARGET_LINK_LIBRARIES(target library1<debug | optimized> library2...)

這個指令可以用來為target添加需要鏈接的共享庫,本例中是一個可執行文件,但是同樣可以用於為自己編寫的共享庫添加共享庫鏈接。為了解決我們前面遇到的HelloFunc未定義錯誤,我們需要作的是向src/CMakeLists.txt中添加如下指令:

TARGET_LINK_LIBRARIES(main hello)

也可以寫成

TARGET_LINK_LIBRARIES(main libhello.so)

這里的hello指的是我們上一節構建的共享庫libhello.

進入build目錄重新進行構建。

cmake ..
make

這是我們就得到了一個連接到libhello的可執行程序main,位於build/src目錄,運行main的結果是輸出:

Hello World

讓我們來檢查一下main的鏈接情況:

ldd src/main
linux-gate.so.1 => (0xb7ee7000)
libhello.so.1 => /usr/lib/libhello.so.1 (0xb7ece000)
libc.so.6 => /lib/libc.so.6 (0xb7d77000)
/lib/ld-linux.so.2 (0xb7ee8000)

可以清楚的看到main確實鏈接了共享庫libhello,而且鏈接的是動態庫libhello.so.1

那如何鏈接到靜態庫呢?

方法很簡單:

將TARGET_LINK_LIBRRARIES指令修改為:

TARGET_LINK_LIBRARIES(main libhello.a)

重新構建后再來看一下main的鏈接情況

ldd src/main
linux-gate.so.1 => (0xb7fa8000)
libc.so.6 => /lib/libc.so.6 (0xb7e3a000)
/lib/ld-linux.so.2 (0xb7fa9000)

說明,main確實鏈接到了靜態庫libhello.a

 

6、特殊的環境變量 CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH

務必注意,這兩個是環境變量而不是cmake變量。使用方法是要在bash中用export或者在csh中使用set命令設置或者CMAKE_INCLUDE_PATH=/home/include cmake ..等方式。

這兩個變量主要是用來解決以前autotools工程中--extra-include-dir等參數的支持的。也就是,如果頭文件沒有存放在常規路徑(/usr/include, /usr/local/include等),則可以通過這些變量就行彌補。我們以本例中的hello.h為例,它存放在/usr/include/hello目錄,所以直接查找肯定是找不到的。前面我們直接使用了絕對路徑INCLUDE_DIRECTORIES(/usr/include/hello)告訴工程這個頭文件目錄。為了將程序更智能一點,我們可以使用CMAKE_INCLUDE_PATH來進行,使用bash的方法如下:

export CMAKE_INCLUDE_PATH=/usr/include/hello然后在頭文件中將INCLUDE_DIRECTORIES(/usr/include/hello)替換為:

FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)

上述的一些指令我們在后面會介紹。這里簡單說明一下,FIND_PATH用來在指定路徑中搜索文件名,比如:

FIND_PATH(myHeader NAMES hello.h PATHS /usr/include/usr/include/hello)

這里我們沒有指定路徑,但是,cmake仍然可以幫我們找到hello.h存放的路徑,就是因為我們設置了環境變量CMAKE_INCLUDE_PATH。如果你不使用FIND_PATH,CMAKE_INCLUDE_PATH變量的設置是沒有作用的,你不能指望它會直接為編譯器命令添加參數-I<CMAKE_INCLUDE_PATH>。以此為例,CMAKE_LIBRARY_PATH可以用在FIND_LIBRARY中。同樣,因為這些變量直接為FIND_指令所使用,所以所有使用FIND_指令的cmake模塊都會受益。

 

7、小節

本節我們探討了:

如何通過INCLUDE_DIRECTORIES指令加入非標准的頭文件搜索路徑。

如何通過LINK_DIRECTORIES指令加入非標准的庫文件搜索路徑。

如果通過TARGET_LINK_LIBRARIES為庫或可執行二進制加入庫鏈接。

並解釋了如果鏈接到靜態庫。

到這里為止,您應該基本可以使用cmake工作了,但是還有很多高級的話題沒有探討,比如編譯條件檢查、編譯器定義、平台判斷、如何跟pkgconfig配合使用等等。

到這里,或許你可以理解前面講到的“cmake的使用過程其實就是學習cmake語言並編寫cmake程序的過程”,既然是“cmake語言”,自然涉及到變量、語法等。

下一節,我們將拋開程序的話題,看看常用的CMAKE變量以及一些基本的控制語法規則。

未完,待續。。。。

 


 

這里關於LINK_DIRECTORIES似乎有個bug。基本上按照上述所寫方法,只是我沒有將測試用的hello.h和libhello.so安裝在系統的usr/lib目錄下。於是我就需要用到這個LINK_DIRECTORIES指定我的庫文件的目錄。main.c的CMakeLists.txt如下:

ADD_EXECUTABLE(main main.c)
FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)
TARGET_LINK_LIBRARIES(main libhello.so)
LINK_DIRECTORIES(/home/wideking/cmakeDemo/libhello/bulid)

在編譯的時候它就是找不到我的libhello.so文件。goole了一下,有人試過將ADD_EXECUTABLE和LINK_DIRECTORIES互換了下位置就行了。我也試了試,提示錯誤,必須先有main,才能有庫的鏈接,確實也應該這樣。於是再在后面增加一行ADD_EXECUTABLE就變成了這樣:

ADD_EXECUTABLE(main main.c)
FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)
TARGET_LINK_LIBRARIES(main libhello.so)
LINK_DIRECTORIES(/home/wideking/cmakeDemo/libhello/bulid)
ADD_EXECUTABLE(main main.c)

雖然有構建的時候有warning,但是這樣才能正常的使用。我是在cmake2.6版本中測試的。不知道這個是我沒理解對還是咋的。。。。

那個warning就是不得不多添加了一次ADD_EXECUTABLE出來的

 


 

cmake中lib庫的路徑設置

比如目錄結構如下

project/utils

project/bin/lib

project/login/remote/control/src

project/login/remote/control/build

構造control工程

LINK_DIRECTORIES(../../../bin/lib)

這里的相對路徑並不是相對於源碼路徑(CMakeLists.txt路徑),而是相對於執行命令的路徑(build目錄),向上三層目錄結構。

 

src目錄下是源代碼,在build目錄下執行make,那么這個相對路徑就是相對於build目錄。

 

而頭文件的路徑則是相對於源碼的路徑(CMakeLists.txt路徑)

INCLUDE_DIRECTORIES(../../utils)向上2層目錄

 指定頭文件搜索路徑
    INCLUDE_DIRECTORIES( "../../xxxx" )   // 注意:括號里的相對路徑是相對CMakeLists.txt的。

指定庫的連接路徑
   LINK_DIRECTORIES(" ../../xxxx ")    // 括號里的相對路徑是相對執行make動作的目錄為基准的。

 

 

《CMake實踐》筆記一:PROJECT/MESSAGE/ADD_EXECUTABLE

《CMake實踐》筆記二:INSTALL/CMAKE_INSTALL_PREFIX

《CMake實踐》筆記三:構建靜態庫與動態庫 及 如何使用外部共享庫和頭文件 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM