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。