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。