使用CMake构建工程的一个原因是不希望Windows上有一个工程文件,Linux又单独配置一套工程文件。我们希望对于整个工程只配置一便,就可以直接编译运行。CMake本身也具备这样的特性,脚本只用写一次,在其他平台能自动构建工程。这里谈的跨平台主要是如何组织工程和编写CMake来针对Windows和Linux进行一些特殊的处理,在这里说说我常用的几种办法
介绍这些方法的前提是有一些代码只能在Windows上跑,例如调用Win32 API或者使用GDI/GDI+,而在Linux上也有一些Windows不支持的代码。我们就需要一些方法来隔离开这两套代码。假设现在有一个项目,它有一套共同的接口对外提供功能,而在Windows上和Linux上各有一份代码来实现这些接口。可以假设有一套图形相关的功能,对外采用统一的接口,具体实现时Windows上使用GDI+,而Linux上使用其他方案来实现。现在整个项目的目录结构如下
.
├── include
└── platform
├── linux
└── windows
include 目录用来对外提供接口,是导出的头文件。platform隔离了Windows和Linux上的实现代码。
使用宏来控制
我们知道Windows和Linux平台有特定编译器定义的宏,根据这些宏是否定义我们可以知道当前是在Linux还是Windows上编译,是需要编译成32位或者64位程序,又或者编译成debug版本或者release版本。例如下面是我用的简单的判断版本的方式
#define MY_PLATFORM_WINDOWS 0
#define MY_PLATFORM_LINUX 1
#define MY_PLATFORM_APPLE 2
#define MY_PLATFORM_ANDROID 3
#define MY_PLATFORM_UNIX 4
#define MY_ARCH32 1
#define MY_ARCH64 2
#if defined(_WIN32) || defined(_WIN64)
#define MY_PLATFORM MY_PLATFORM_WINDOWS
#ifdef _WIN64
#define PLATFORM_NAME "Windows 64-bit"
#define MY_ARCH MY_ARCH64
#else
#define PLATFORM_NAME "Windows 32-bit"
#define MY_ARCH MY_ARCH32
#endif
#elif defined(__APPLE__)
#include "TargetConditionals.h"
#define MY_PLATFORM MY_PLATFORM_APPLE
#ifdef ARCHX64
#define MY_ARCH MO_ARCH64
#else
#define MY_ARCH MO_ARCH32
#endif
#if TARGET_IPHONE_SIMULATOR
#define PLATFORM_NAME "iOS Simulator"
#elif TARGET_OS_IPHONE
#define PLATFORM_NAME "iOS"
#elif TARGET_OS_MAC
#define PLATFORM_NAME "macOS"
#endif
#elif defined(__linux__)
#define MY_PLATFORM MY_PLATFORM_LINUX
#if defined (ARCHX64) || defined (__x86_64__)
#define MY_ARCH MY_ARCH64
#else
#define MY_ARCH MY_ARCH32
#endif
#define PLATFORM_NAME "Linux"
#elif defined(__unix__)
#ifdef ARCHX64
#define MY_ARCH MY_ARCH64
#else
#define MY_ARCH MY_ARCH32
#endif
#define PLATFORM_NAME "Unix"
#define MY_PLATFORM MY_PLATFORM_UNIX
#else
#error "Unknown platform"
#endif
上面代码根据一些常见的编译器宏来决定是什么版本,并且根据不同的版本将MY_PLATFORM
进行赋值。后面只需要使用 MY_PLATFORM
进行版本判断即可。同样的关于架构使用 MY_ARCH
来判断。
例如根据架构来定义不同的数据长度
#if (MY_PLATFORM == MY_PLATFORM_WINDOWS)
typedef __int64 MY_INT64;
typedef unsigned __int64 MY_UINT64;
#else
typedef long long MY_INT64;
typedef unsigned long long MY_UINT64;
#endif
#if (MY_ARCH == MY_ARCH64)
typedef MY_UINT64 MY_ULONG_PTR;
typedef MY_INT64 MY_INT_PTR;
#else
typedef MY_UINT MY_ULONG_PTR;
typedef MY_INT MY_INT_PTR;
#endif
定义的常见的数据结构之后,对于一些接口的视线就可以利用宏来隔开
// platform/windows/image.c
#if (MY_PLATFORM == MY_PLATFORM_WINDOWS)
// todo something
#endif
// platform/linux/image.c
#if (MY_PLATFORM == MY_PLATFORM_LINUX)
// todo something
#endif
这样我们可以利用上一节介绍过的 cmake
的 file
或者 aux_source_directory
将整个platform目录都包含到工程里面。
使用cmake来判断版本
除了在C/C++ 源码中利用编译器特定的宏来判断版本,其实CMake自身也有一些方式来判断编译的版本。
CMake 检测操作系统使用 CMAKE_SYSTEM_NAME
来判断。这里要提一句,CMake中的变量本质上都是一个字符串值,没有严格的区分类型,所以 set(variable 1)
和 set(variable "1")
在底层存储都是字符串 1。所以cmake在定义变量的时候可以不使用双引号,但是对于特殊的字符串,例如带有空格的字符串,为了避免语法上的歧义,可以带上双引号。
虽然说底层存储的都是字符串,但是在上层判断变量是否相等的时候却又区分数字和字符串。判断变量是否等于某个值可以使用 EQUAL
或者 STREQUAL
。EQUAL
是用来判断数字类型是否相等,一般判断版本号或者数字参数。而 STREQUAL
来判断字符串是否相等,一般用来判断配置选项、路径、平台标识符。例如这里的 CMAKE_SYSTEM_NAME
就需要采用 STREQUAL
来判断
# 检测平台
set(PLATFORM_WINDOWS 1)
set(PLATFORM_LINUX 2)
set(PLATFORM_MACOS 3)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(PLATFORM ${PLATFORM_WINDOWS})
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(PLATFORM ${PLATFORM_LINUX})
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(PLATFORM ${PLATFORM_MACOS})
endif()
判断架构可以使用 CMAKE_SIZEOF_VOID_P
。顾名思义,它表示一个void*
指针变量的大小,8位就是64位,4位则是32位架构。
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(PLATFORM_ARCH "x64")
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(PLATFORM_ARCH "x86")
else()
message(FATAL_ERROR "Unsupported architecture pointer size: ${CMAKE_SIZEOF_VOID_P}")
endif()
至于判断当前编译的版本是debug 还是 release 可以使用 CMAKE_BUILD_TYPE
来判断,它的值主要有4个,分别是 Debug
、RelWithDebInfo
、MinSizeRel
、Release
。它们四个各有特色。其中 RelWithDebInfo
是带有符号表的发布版,便于调试,它的优化级别最低。MinSizeRel
和Release
在优化上各有千秋,前者追求最小体积,后者追求最快的速度,所以后者有时候会为了运行速度添加一些额外的内容导致体积变大。
我们可以在cmake
文件中判断对应的值以便做出一些额外的设置。例如
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_compile_definitions(-D_DEBUG)
else()
add_compile_definitions(-DNDEBUG)
endif()
有了这些基础,我们可以在不同的条件下,定义不同的编译宏,然后根据编译宏的不同在C/C++ 源码中判断这些宏从而隔离不同平台的实现代码
通过cmake list 来隔离不同平台的代码
使用 file
或者 aux_source_directory
的到的是一个源代码文件的列表。我们可以操作这个列表来达到控制编译文件的需求。
cmake 中针对列表的操作符是 list,它的定义如下:
Reading
list(LENGTH <list> <out-var>)
list(GET <list> <element index> [<index> ...] <out-var>)
list(JOIN <list> <glue> <out-var>)
list(SUBLIST <list> <begin> <length> <out-var>)
Search
list(FIND <list> <value> <out-var>)
Modification
list(APPEND <list> [<element>...])
list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>)
list(INSERT <list> <index> [<element>...])
list(POP_BACK <list> [<out-var>...])
list(POP_FRONT <list> [<out-var>...])
list(PREPEND <list> [<element>...])
list(REMOVE_ITEM <list> <value>...)
list(REMOVE_AT <list> <index>...)
list(REMOVE_DUPLICATES <list>)
list(TRANSFORM <list> <ACTION> [...])
Ordering
list(REVERSE <list>)
list(SORT <list> [...])
官方提供了这么一些操作list的操作符,但是在这个需求中我们只需要两个操作符APPEND
和 REMOVE_ITEM
即可。后面的参数分别是源列表,以及需要增加或者删除的项,它们都可以传入多项。但是删除时它是根据传入字符串,从列表中进行字符串比较,如果相等则进行删除。所以在传入路径的时候需要特别注意,不能一个传入全路径一个传入相对路径或者一个传入 /
开头的路径,另一个传入 ~
开头的路径。
上述两个操作都可以传入多个单个的字符串也可以传入一个列表。
如果我们有下列目录结构
src/platform/windows
src/platform/linux
src/*.cpp
src/other/*.cpp
也就说我们将不同平台的代码放入到src目录,并且src目录也有其他代码,我们如果使用 file
操作符来查找src目录中的源码文件必定会包含两个平台的实现代码。这个时候就可以考虑使用REMOVE_ITEM 根据平台来舍弃一些代码,例如
file(GLOB_RECURSE
SOURCES
${PROJECT_SOURCE_DIR}/src/*.c
)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
file(GLOB_RECURSE
NOT_INCLUDE
${PROJECT_SOURCE_DIR}/src/platform/linux/*.cpp
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
file(GLOB_RECURSE
NOT_INCLUDE
${PROJECT_SOURCE_DIR}/src/platform/windows/*.cpp
)
endif()
list(
REMOVE_ITEM SOURCES
SOURCE
${NOT_INCLUDE}
)
又或者我们采用最上面的给出的目录结构,也就是说platform 目录位于src目录之外,相对于src来说是额外添加的代码文件,那么就可以使用 APPEND
来进行添加
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
aux_source_directory(${PROJECT_SOURCE_DIR}/platform/windows PLATFORM_SOURCE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
aux_source_directory(${PROJECT_SOURCE_DIR}/platform/linux PLATFORM_SOURCE)
endif()
list(APPEND SOURCES ${PLATFORM_SOURCE})
通过 toolchain 文件来组织平台特殊配置
cmake 允许我们在生成Makefile的时候指定toolchain 文件来实现一些自定义的配置。例如可以根据平台的不同将生成路径指定在对应的toolchain中。
toolchain 的语法与cmake语法相同,例如针对Windows和Linux可以创建 win32_toolchain.cmake
win64_toolchain.cmake
linux_86_toolchain.cmake
和 linux_x64_toolchain.cmake
文件来区别
我还是以上一篇文章中多工程嵌套的例子作为示例来演示如何使用,它的目录结构如下
.
├── calc
│ ├── add.cpp
│ ├── CMakeLists.txt
│ ├── div.cpp
│ ├── mult.cpp
│ └── sub.cpp
├── CMakeLists.txt
├── include
│ ├── calc.h
│ └── sort.h
├── sort
│ ├── CMakeLists.txt
│ ├── insert_sort.cpp
│ └── select_sort.cpp
├── test_calc
│ ├── CMakeLists.txt
│ └── main.cpp
└── test_sort
├── CMakeLists.txt
└── main.cpp
这个例子我们只需要改动根目录下的生成库和可执行程序的路径。
cmake_minimum_required(VERSION 3.15)
project(test)
add_subdirectory(sort)
add_subdirectory(calc)
add_subdirectory(test_calc)
add_subdirectory(test_sort)
这个文件只需要保留最基础的配置即可,而生成程序的路劲都在 toolchain 中。
下面是 linux_x86_toolchain.cmake 文件的内容
set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/linux/x86)
set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/linux/x86)
set(HEADPATH ${PROJECT_SOURCE_DIR}/include)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CALCLIB calc_d)
set(SORTLIB sort_d)
set(CALCAPP test_calc_d)
set(SORTAPP test_sort_d)
else()
set(CALCLIB calc)
set(SORTLIB sort)
set(CALCAPP test_calc)
set(SORTAPP test_sort)
endif()
set(CMAKE_SYSTEM_PROCESSOR i686)
set(CMAKE_C_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE)
set(CMAKE_EXE_LINKER_FLAGS "-m32 -L/usr/lib32" CACHE STRING "" FORCE)
这个文件我们定义了debug和release的库名称和生成的路径,并且指定相关参数用于生成32位程序。
CMAKE 中定义了一堆 CMAKE_LANGUAGE_FLAGS 这些都是相关工具的参数,这里的FLAGS 分别是 gcc 和 ld 编译链接的参数。
在使用时直接用命令 cmake .. -DCMAKE_TOOLCHAIN_FILE=../linux_x32_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug
生成Makefile。
Windows平台上上面的参数稍微有些差距。例如下面是 windows_x32_toolchain.cmake
的定义
# 静态库生成路径
set(LIBPATH ${PROJECT_SOURCE_DIR}/lib/windows/x86)
# 可执行程序的存储目录
set(EXECPATH ${PROJECT_SOURCE_DIR}/bin/windows/x86)
# 头文件路径
set(HEADPATH ${PROJECT_SOURCE_DIR}/include)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
# calc库名称
set(CALCLIB calc_d)
# sort 库名称
set(SORTLIB sort_d)
# 测试程序的名字
set(CALCAPP test_calc_d)
set(SORTAPP test_sort_d)
else()
# calc库名称
set(CALCLIB calc)
# sort 库名称
set(SORTLIB sort)
# 测试程序的名字
set(CALCAPP test_calc)
set(SORTAPP test_sort)
endif()
set(CMAKE_GENERATOR_PLATFORM "Win32" CACHE STRING "Target Platform")
# 指定32位编译器路径
set(CMAKE_C_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe")
set(CMAKE_CXX_COMPILER "$ENV{VCToolsInstallDir}/bin/Hostx86/x86/cl.exe")
set(CMAKE_SYSTEM_PROCESSOR x86)
需要注意的是 CMAKE_GENERATOR_PLATFORM
对应的是VS 中的解决方案平台,也就是 Win32
和 x64
这两个选项。 而 CMAKE_SYSTEM_PROCESSOR
对应的是VS中的目标计算机选项,一般是X86
、X64
或者 ARM64
和 AMD64
。
我们可以使用命令 cmake -G "Visual Studio 15 2017" -A "win32" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="..\windows_x86_toolchain.cmake" ..
这里指定使用 VS 2017
进行构建,构建架构是32位,版本是Debug。
命令成功执行之后会生成一个.sln 文件,我们可以用VS打开然后在VS中编译,也可以执行使用命令 cmake --build . --config Debug
来编译。一般来说我习惯使用后者,过去使用vs 打开.sln 可以在vs中进行开发,如今vs 已经可以打开并编译cmake工程,所以现在我基本不使用 .sln 文件了,除非公司项目要求使用 .sln。
好了,目前我掌握的关于cmake的内容就是这些了,我利用这些知识已经完成了公司项目的跨平台开发和部署。后续如果有新的需求说不定我会学点新的内容,到时候再来更新这一系列文章吧!!!
评论 (0)