Loading

《CMake Best Practice》第 3 章笔记--Creating a CMake Project

初始化项目

CMake 项目推荐的文件组织方式:

image-20220716164524800

  • build:构建目录,用来存放 build 文件和二进制文件。
  • include/project_name:存放所有项目外可以公开访问的头文件。因为通常 include 一个头文件的形式是这样的 #include <project_name/somefile.h> ,所以有必要在 include 文件夹下创建一个 project_name 子文件夹,这样外部引用头文件时,可以很容易分辨出来这个头文件所属的项目。
  • src:存放所有私有的 .h 头文件和 .cpp 源文件。
  • CMakeLists.txtCMake 根文件。

嵌套项目

当将项目相互嵌套时,每个项目都应该映射上面的文件结构,并且为每个子项目编写 CMakeLists.txt,以便子项目可以独立构建。

每个子项目的 CMakeLists.txt 都应该包含 cmake_minimum_requiredproject 指令。

一个嵌套项目可能有下面的文件结构:

image-20220716165557101

创建一个hello world可执行文件

src 文件夹下只包含一个 main.cpp 文件:

#include <iostream>
int main(int, char **) {
std::cout << "Welcome to CMake Best Practices\n";
return 0;
}

CMakeLists.txt 文件内容如下:

cmake_minimum_required(VERSION 3.21)
project(
	hello_world_standalone
	VERSION 1.0
	DESCRIPTION "A simple C++ project"
	HOMEPAGE_URL https://github.com/PacktPublishing/CMake-Best-Practices
	LANGUAGES CXX
)
add_executable(hello_world)
target_sources(hello_world PRIVATE src/main.cpp)

target_sources 指定了可执行文件 hello_world 的源文件。因为 CMake 中的各个 target 之间可能会有依赖关系,因此 PRIVATE 的作用是:

PRIVATE defines that the sources are only used to build this target and not for any dependent targets.

除非你正在构建一个巨大的单体应用程序,否则应该使用 library 来模块化和分发代码。

创建一个库

cmake_minimum_required(VERSION 3.21)
project(
	ch3.hello_lib
	VERSION 1.0
	DESCRIPTION "A simple C++ project to demonstrate creating executables and libraries in CMake"
	LANGUAGES CXX)
add_library(hello)
target_sources(hello PRIVATE src/hello.cpp src/internal.cpp)
target_compile_features(hello PUBLIC cxx_std_17)
target_include_directories(
	hello
	PRIVATE src/hello
	PUBLIC include)

add_library 指令中应该传递 STATIC 或者 SHARED 来指定库的类型。但是如果没有显式的传递,那么该库的类型由用户通过设置 BUILD_SHARED_LIBS 变量的值来指定库的类型。注意:该变量只能由用户传递,不可以在 CMake 文件中指定。

target_sources 指令中使用 PRIVATE 或者 PUBLIC 或者 INTERFACE 指定源文件的可见性。通常源文件应该指定成 PRIVATE。它们三者之间的区别是:

  • PRIVATE,源文件只会被用来编译 hello 库自己。
  • PUBLIC,源文件不仅被用来编译 hello 自己,还会被用来编译任何一个链接了 hello 库的 target
  • INTERFACE,源文件不会被用来编译 hello 库自己,但是会被用来编译任何一个链接了 hello 库的 target

使用 target_include_directories 设置 hello 库的 include 目录。

共享库的符号可见性

不同的编译器对共享库的符号可见性有不同的处理方法,gccclang 把所有的符号都当成可见的,除非显式的指定,否则 MSVC 会把所有符号都隐藏。

改变默认的可见性

如果需要改变默认的可见性,需要把 <LANG>_VISIBILITY_PRESET 属性设置为 HIDDEN,可以针对全局设置该属性,也可以针对某一个具体的 target 设置该属性,对于不同的语言,<LANG> 应该是不同的值,比如 CXX 或者 C

设置完这个属性之后,除非特殊指定,否则所有的符号都是默认隐藏的。可以在符号之前添加一个预处理器定义(其实就是一个宏定义),改变这个符号的可见性,比如:

class HELLO_EXPORT Hello {
…
};

CMAKE 提供了 generate_export_header 指令来生成这个预处理器定义(如 HELLO_EXPORT),使用这个指令之前,必须要先 include GenerateExportHeader 这个模块。

看一个例子:

add_library(hello SHARED)
set_property(TARGET hello PROPERTY CXX_VISIBILITY_PRESET "hidden")
set_property(TARGET hello PROPERTY VISIBILITY_INLINES_HIDDEN TRUE)
include(GenerateExportHeader)
generate_export_header(hello EXPORT_FILE_NAME export/hello/export_hello.hpp)
target_include_directories(hello PUBLIC "${CMAKE_CURRENT_BINARY_DIR} /export")

VISIBILITY_INLINES_HIDDEN 属性设置为 TRUE 来隐藏内联成员函数。

generate_export_header 指令将在 CMAKE_CURRENT_BINARY_DIR/export/hello 文件夹下生成一个 export_hello.hpp 文件,当你想将一个符号设置为可见时,应该 #include <hello/export_hello.hpp>

更多的参考,可见:https://cmake.org/cmake/help/latest/module/GenerateExportHeader.html

仅包含头文件的库(header-only library)

创建一个仅包含头文件的库的实例如下:

project(ch3_hello_header_only VERSION 1.0
	DESCRIPTION "Chapter 3 header-only example" 
	LANGUAGES CXX)
add_library(hello_header_only INTERFACE)
target_include_directories(hello_header_only INTERFACE include/)
target_compile_features( hello_header_only INTERFACE cxx_std_17)

add_library 指令中需要将仅包含头文件的库指定为 INTERFACE,因为 header-only library 不需要被编译,所以它也没有 source list,自然不需要target_sources() 指令。

对象库

使用对象库可以将你的代码分割成一个个可重用的部分。举个例子,你可能在一个可执行文件和一个单元测试用例中都用到了同一个库的代码,无论是使用静态库还是动态库,你都需要编译这个库两次。对象库可以避免代码的多次编译,因为对象库只会将代码进行编译,不会执行链接动作

CMAKE 3.12 开始,你可以像使用普通的库一样使用对象库,你可以直接使用 target_link_libraries 来链接一个对象库。

使用库

add_subdirectory(hello_lib)
add_subdirectory(hello_header_only)
add_subdirectory(hello_object)
add_executable(chapter3)
target_sources(chapter3 PRIVATE src/main.cpp)
target_link_libraries(chapter3 PRIVATE hello_header_only hello hello_object)

使用 target_link_libraries 来给一个 target 链接其它的库。在链接时同样有三种访问控制符:

  • PRIVATE,库只会被用来链接 chapter3 可执行文件自己。
  • PUBLIC,库不仅被用来链接自己,还会被用来链接任何一个链接了 hello 库的 target
  • INTERFACE,源文件不会被用来链接库自己,但是会被用来链接任何一个链接了 hello 库的 target

设置编译器和链接器选项

分别使用 target_compile_optionstarget_link_options 来设置编译器和链接器的选项。

但是不同的编译器传递选项的方式不同,比如 GCCClang- 传递参数,而 MSVC/ 传递参数。使用生成器表达式可以很好的解决这个问题。比如下面的例子:

target_compile_options(hello PRIVATE 
	$<$<CXX_COMPILER_ID:MSVC>:/SomeOption>
	$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-someOption>
)

关于生成器表达式可以看这篇文章:https://zhuanlan.zhihu.com/p/437404485

添加预处理器定义

使用 target_compile_definitions 添加预处理器定义。

比如,为源代码添加一个宏定义:

add_compile_definitions(MG_ENABLE_OPENSSL=1)

这相当于:

#define MG_ENABLE_OPENSSL 1
posted @ 2022-08-12 18:49  cclemontree  阅读(570)  评论(0)    收藏  举报