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。