CMake-based C++工程

本文参考medium作者Aakash Mallik关于c++开发的文章。

1. https://medium.com/heuristics/c-application-development-part-1-project-structure-454b00f9eddc
2. https://medium.com/heuristics/c-application-development-part-2-cmakelists-txt-e415b5b387dc
3. https://medium.com/heuristics/c-application-development-part-3-cmakelists-txt-from-scratch-7678253e5e24
4. https://www.learncpp.com/cpp-tutorial/a1-static-and-dynamic-libraries/
5. https://medium.com/@onur.dundar1/cmake-tutorial-585dd180109b
6. https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/

在开发C++大工程时,有两件事情要注意:

  1. Maintaining a project structure
  2. Dealing with third-party libraries

关注第一件事情:Maintaining a project structure

通常的c++工程结构如下:

  1. CMakeLists.txt
  2. include文件夹
  3. src文件夹
  4. libs文件夹
  5. tests文件夹。
include文件夹

传统上,include文件夹是用于放header files, 但是modern practice 建议include文件夹必须strictly contain headers that need to be exposed publicly. 我们在include文件夹下,还特地加了一个与project名字相同的文件夹。这样做的目的是,由于include文件夹主要是为了方便外用的(供别人调用你写的这个library),所以我么希望当别人调用该project用于外用时是这样的:#include <Project_Name/public_header.h>,而不是#include <public_header.h>的。

src文件夹

src文件夹包含所有的源代码,以及所有那些仅用于internal use的header files。基本上,如果你注意third-party libraries时,基本都有类似的结构。

libs文件夹

libs文件夹包含了所有我们需要用到的third-party libraries。通常我们有两种方式来使用third-party libraries in C++:Static或者Dynamic。libs文件夹只包含通过static方式使用的third-party libraries。

tests文件夹

用于做unit tests或者integration tests的代码都放在这里。

CMakeLists.txt文件

CMakeLists.txt是告诉CMake命令what to do的配置文件。需要注意的是CMake is not a build system, but a build system的生成器. 要理解这句话什么意思,你需要懂得Make and CMake的区别

为了很好地理解,让我们先git clone该C++工程https://github.com/AakashMallik/sample_cmake。通常你从github上clone一个C++工程,为了运行该工程,你需要做一下在terminal走以下五步:

cd path_of_the_downloaded_project
mkdir build
cd build
cmake ..
make
mkdir build在做什么

本质上只是创建了一个文件夹。所以Technically speaking, you can get away without creating this directory. This is done just to keep your code clean and mess-free. So for now take my word for it and try to understand what is this mess I am talking about.

cmake ..在做什么

CMake单纯是用于生成Make file。那么Make file是用来干什么的呢?

首先我们知道g++编译工作一般分为四个步骤:

  1. 预处理(Preprocessing): g++ -E test.cpp -o test.i //生成预处理后的.i文件, -E代表只激活预处理
  2. 编译(Compilation):g++ -S test.i -o test.s //生成汇编.s文件,-S代表只激活预处理和编译
  3. 汇编(Assembly):g++ -c test.s -o test.o //生成二进制.o文件,-c只激活预处理,编译,和汇编
  4. 链接(Linking):g++ test.o -o test.out //生成二进制.out可执行文件

假设我们有main.cpp,a.cpp,a.h,b.cpp,b.h,并且main.cpp包含了main() function,并且该main() function就依赖于a.cpp b.cpp。那么make file相当于要完成两步:

  1. compiling(包含了预处理、编译和汇编): 即对依赖的文件a.cpp b.cpp,和main.cpp生成对应的三个目标文件a.o,b.o,main.o
  2. linking: 将a.o,b.o与main.o联系起来生成binary/executable file

具体命令是:

g++ -c a.cpp      //-c只激活预处理,编译,和汇编
g++ -c b.cpp      //-c只激活预处理,编译,和汇编
g++ -c main.cpp   //-c只激活预处理,编译,和汇编
g++ a.o b.o main.o -o binary //link目标文件,生成可执行二进制文件

但问题是每次你对其中一个任何代码有更新,你都要重新compile和link。另一个问题是,如果你的工程很大,有几千个cpp代码文件呢?于是就想出一个办法:调用a build system来自动化完成这件事情,于是make这个工具应运而生。有了这个make工具,你只需要:

  • write a make file and tell it what commands to execute
  • run the make tool pointing it to the location of the make file.

但是这里又有新的问题,写make file很麻烦,于是就有了cmake这个工具。它通过写更为简单CMakeLists.txt(而不是直接写make file)用来自动生成make file。所以我们说cmake是build system(指的是make file)的生成器。所以刚才我们在运行一个工程文件需要走的步骤中的”cmake ..”就是在build的上一层目录path_of_the_downloaded_project路径下找到CMakeLists.txt,然后cmake工具依据CMakeLists.txt在build路径下来生成make file。

cd path_of_the_downloaded_project
mkdir build
cd build
cmake ..
make

总结:我们通过cmake命令根据提供的CMakeLists.txt文件路径信息(当不指明路径,就默认是当前路径。)来读CMakeLists.txt文件,在当前路径下生成了make file(a build system),然后make命令根据提供的make路径信息(当不指明路径,就默认是当前路径)来读make file,将整个c++工程在当前路径下生成对应的二进制可执行文件。

如何写CMakeLists.txt?

一个好的c++工程应该具备一下是三个特点:

  1. 方便你随时编译并生成二进制可执行文件
  2. 方便别人调用你的工程当做third-party library(Expose a header file to let people use your code as a third-party library)
  3. Able to use third-party libraries in your code.

我们继续用https://github.com/AakashMallik/sample_cmake项目来做为例子,项目结构如下:

下面将展示如何手动和自动化两种方式(通过写CMakeLists.txt文件用cmake和make来做)来build整个工程。

手动来build生成二进制可执行文件
g++ ../src/game_engine.cpp ../src/game_interface.cpp ../src/main.cpp -I ../src -I ../include -I ../libs/Logger/include -I ../libs/Randomize/include -L ../libs/Logger -l logger -L ../libs/Randomize -l randomize

以上命令可以看做是有四步:

  • Path to the files that you have written and wish to compile.
  • Path to all the header files, including the ones used from 3rd party libraries.
  • Paths to third-party libraries’ .a files.
  • Names of the .a files that we want the compiler to link with our code.
自动化build生成二进制可执行文件

CMakeLists.txt文件可以分为6个主要部分:

  • Flags
  • Files
  • Include
  • Targets
  • External libraries
  • Unit Testing

第一个件事是写出cmake tool要求的版本,如果你的电脑装了比较旧的cmake tool,那么我们就无法cmake成功,需要更新了。

第二件事就是定义project name(不一定与main.cpp的名字相同)

第三件事指出flags,就是告诉cmake命令你打算用哪个compiler和compiler的版本来build你的project。如果没有写明flags,cmake会自动找gcc compiler,即 it will pick the best fit on its own.

第四件事是include_directories。这里需要给出全部所要用到的head files所在的路径,那么显然也包含了要调用的third-party libraries的head files所在的路径。

第五件事是add_executable,用来告诉cmake命令你要的输出是什么。我们这里需要的是binary的,后面列出的三个文件就是你的源代码文件, the same way as you do while compiling them manually.

第六件事是add_subdirectory,add_library。这里就是前面提到的make file要做的两步compiling和linking两步中的linking。刚才我们在第四件事上已经给出third-party libraries的head files在哪里,而我们知道linking这一步是需要给出third-party libraries的文件(By convention, library files are named with ‘lib’ as a prefix or a suffix, 例如loggerlib,或者liblogger)在哪里,所以add_subdirectory就是加third-party libraries的路径。

在c++工程中,可以使用两种类型的libraries:

  • static ( .a files ): also known as an archive.
    • Consists of routines that are compiled and linked directly into your program. When you compile a program that uses a static library, all the functionality of the static library that your program uses becomes part of your executable.
    • On Windows, static libraries typically have a .lib extension, whereas, on Linux, static libraries typically have an .a (archive) extension. 
    • One advantage of static libraries is that you only have to distribute the executable in order for users to run your program. Because the library becomes part of your program, this ensures that the right version of the library is always used with your program.
    • because static libraries become part of your program, you can use them just like the functionality you’ve written for your own program.
    • On the downside, because a copy of the library becomes part of every executable that uses it, this can cause a lot of wasted space.
    • Static libraries also can not be upgraded easily — to update the library, the entire executable needs to be replaced.
  • dynamic ( .so files ): also called shared library
    • consists of routines that are loaded into your application at run time.
    • On Windows, dynamic libraries typically have a .dll (dynamic link library) extension, whereas, on Linux, dynamic libraries typically have a .so (shared object) extension. 
    • When you compile a program that uses a dynamic library, the library does not become part of your executable — it remains as a separate unit. 
    • One advantage of dynamic libraries is that many programs can share one copy, which saves space. 
    • Perhaps a bigger advantage is that the dynamic library can be upgraded to a newer version without replacing all of the executables that use it.
    • Because dynamic libraries are not linked into your program, programs using dynamic libraries must explicitly load and interface with the dynamic library.
    • This mechanism can be confusing and makes interfacing with a dynamic library awkward. To make dynamic libraries easier to use, an import library can be used. An import library is a library that automates the process of loading and using a dynamic library. On Windows, this is typically done via a small static library (.lib) of the same name as the dynamic library (.dll). 
如何创建你自己的static third-party libraries是一个重要话题,但是这里先假设我们要用到的两个libraries(Logger and Randomize)是static。而static third-party libraries可以用两种类型:

第一类:CMakeLists.txt + include + src

add_subdirectory( ./libs/Logger )                       target_link_libraries( binary logger )
  • 这里用到的Logger就是这类
  • add the path of Logger directory as a subdirectory
  • tell the compiler to link it to your output.
  • CMake will automatically look for a CMakeLists.txt file inside the subdirectory and run it. It will use the .a file(static library in Linux) to link with your output as mentioned in the second line.
  • the file it will be looking for is not logger.a, but liblogger.a or loggerlib.a.

第二类:.a file + include

add_library(randomize STATIC IMPORTED)                       set_property(TARGET randomize PROPERTY IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/Randomize/librandomize.a)                       target_link_libraries( binary randomize )
  • 这里用到的Randomize就是这类
  • In this case, the library file has already been compiled for you and you don’t need CMake to do it for you.
  • But adding this directory as a subdirectory won’t work as CMake will start looking for a CMakeLists.txt inside the library’s directory and as it doesn’t have one, it will throw an error and won’t compile. 
  • The first one basically tells CMake that we are using a static library name librandomize.a or randomizelib.a. 
  • The second line specifies the path from where it resides. I have used the ${CMAKE_SOURCE_DIR} to demonstrate the use of internal CMake variable. That variable is the path to the CMakeLists.txt in your root directory; the one we run to build our binary.
  • The third line as you might have guessed, links the library to our output.

总结

mkdir build
cd build
cmake ..
make

And just like that you have your output — binary. You can run it now!

./binary

2 Comments

Leave a Comment