CMake 入門/程式庫進階議題
Windows DLL
編輯一般來說建立共享程式庫和建立靜態程式庫並沒有太大的不同,但是 Visual C++ 建立 DLL 時並不會主動建立輸出項目,必須要明確在宣告當中指明 __declspec (dllexport),而使用端也必須明確使用 __declspec (dllimport)。因此若是想建立 so 和 dll 共用的原始碼必須要多費點心思。
不論是用下面何種方式建立 DLL,CMake 都會自動加入 preprocessor 定義 <target_name>_EXPORTS。
set(BUILD_SHARED_LIBS ON) add_library(<target_name> source.c...)
或
add_library(<target_name> SHARED source.c...)
假如我們的 target 名稱叫做 calc,CMake自動加入的 preprocessor 定義就是 calc_EXPORTS,以下 header 片段是常用的一種平台判斷方式。
#if defined(_WIN32) && defined(calc_EXPORTS) # if defined(LIBCALC_INTERNAL) # define LIBCALC_API __declspec (dllexport) # else # define LIBCALC_API __declspec (dllimport) # endif #endif #if !defined(LIBCALC_API) # define LIBCALC_API #endif LIBCALC_API int Square(int x);
在DLL的實作程式碼中加入 LIBCALC_INTERNAL 之定義即可控制輸出名稱。
#define LIBCALC_INTERNAL #include "calc.h" ... LIBCALC_API int Square(int x) { ... }
除此之外,輸出名稱的 name mangling 也是一大問題。通常用 extern "C" 就能確保不會被 C++ 的 mangling 方式處理,但對於和 C 相容的名稱修飾方式卻無濟於事,例如 calling convention 所帶來的名稱修飾就無法靠 extern "C" 解決,唯一的解決方法就是提供 .def 檔。
在 CMake 下可以透過之前提過的編譯選項加入 .def 檔,另外 CMake 也提供了 CMAKE_LINK_DEF_FILE_FLAG 變數,專用於指定傳遞給編譯器的 .def 檔名稱。
控制輸出檔名
編輯在建置程式庫時,我們常常需要加入額外的前、後綴以區別同一個程式庫的不同版本,諸如 release/debug、連結方式、版本號等等,以下就來探討 CMake 提供哪些方法控制輸出檔名。
加入編譯組態後綴
編輯有時候我們需要將 release 和 debug 組態的程式庫放在同一個資料夾下,為了不使名稱衝突,所以要加入後綴。例如
- libfoo.a 表示程式庫為非 debug 組態。
- libfoo-d.a 表示程式庫為 debug 組態。
最簡單的方法就是直接設定 CMAKE_DEBUG_POSTFIX 變數:
set(CMAKE_DEBUG_POSTFIX -d) add_library(foo ${foo_sources}) add_library(bar ${bar_sources})
之後所有不是執行檔的 target 在 debug build 時會自動在輸出檔名後面加上 -d。除了 CMAKE_DEBUG_POSTFIX 之外,其他的編譯組態都有對應的 CMAKE_<CONFIG>_POSTFIX 變數。
若僅需要設定單一個 target 的後綴,可以使用 set_target_properties() 指令設定 target 的 DEBUG_POSTFIX 屬性。
set(CMAKE_DEBUG_POSTFIX -d) add_library(foo ${foo_sources}) add_library(bar ${bar_sources}) set_target_properties(bar PROPERTIES DEBUG_POSTFIX -x)
這裡要注意的是,所有屬性在沒有特別設定的情況下,都會依照相關的全域變數自動設定;若是特別指定 target 的屬性值,屬性的效力將會蓋過全域變數。如上在 debug build 時,foo 的輸出檔名為 libfoo-d.a,但 bar 會變成 libbar-x.a。
讓靜態與共享程式庫檔名一致
編輯若打算在一次建置同時生成靜態和共享程式庫,在 CMake 當中就必須分成兩個獨立的 target。
add_library(foo SHARED ${foo_sources}) add_library(foo-s STATIC ${foo_sources})
在預設的情況下 CMake 自動以 target 名稱當成輸出檔名:
Compilers | foo | foo-s |
---|---|---|
Visual C++ | foo.dll foo.lib |
foo-s.lib |
gcc (MinGW) | libfoo.dll libfoo.dll.a |
libfoo-s.a |
gcc (Unix-like) | libfoo.so | libfoo-s.a |
現實中我們比較希望靜態和共享程式庫同名,只是副檔名不同,如此才會被連結器視為同一個程式庫。透過設定 target 的 OUTPUT_NAME屬性,我們可以讓輸出檔名和 target 名稱脫鉤。
add_library(foo SHARED ${foo_sources}) add_library(foo-s STATIC ${foo_sources}) set_target_properties(foo-s PROPERTIES OUTPUT_NAME "foo" PREFIX "lib" )
這裡要注意一點,如果只有設定 OUTPUT_NAME 屬性,Visual C++ 輸出的靜態程式庫 foo.a 剛好會與 dll 附屬的引入程式庫撞名,額外加入前綴 lib 可以解決此問題。經過這一番修改,現在的輸出檔名為:
Compilers | foo | foo-s |
---|---|---|
Visual C++ | foo.dll foo.lib |
libfoo.lib |
gcc (MinGW) | libfoo.dll libfoo.dll.a |
libfoo.a |
gcc (Unix-like) | libfoo.so | libfoo.a |
還有,不論一個 target 實際的檔名如何變更,我們在 CMakeLists 當中仍然是以同樣的 target 名稱加以操作。
add_executable(app ${app_sources}) target_link_libraries(app foo-s)
加入 SO 版號
編輯SO 的版本比較複雜,在 CMake 當中提供了兩個屬性:
- VERSION: 這是程式庫的「實做」版本,和一般的軟體版本一樣採用 major.minor.patch 格式。
- SOVERSION: 這指的是程式庫的「介面」版本,會影響連結器找尋適用版本的行為。
這兩個屬性對其他的 target 類型沒有實質上的影響,但對於 SO 卻很重要。
假如我們對 calc 加入以下的屬性:
add_library(calc SHARED calc.c) set_target_properties(calc PROPERTIES VERSION 2.5 SOVERSION 1 )
建置完成後,用 ls 列出產生的檔案會發現此 target 一共生成了三個檔案:
- libcalc.so
- libcalc.so.1
- libcalc.so.2.5
用 ls -al 進一步查詢會顯示三者的關係
- libcalc.so -> libcalc.so.1
- libcalc.so.1 -> libcalc.so.2.5
- libcalc.so.2.5
其中 libcalc.so 和 libcalc.so.1 都是供給連結器辨識用的介面,真正的程式庫內容在 libcalc.so.2.5。