C++ build system - CMake

開發 C++ 會用到很多工具,除了基本的編輯器 Text Editor 跟編譯器 Compiler,還有:

  • Linters
  • Tests
  • Static Code Analysis tools
  • Formatting tools
  • CI / CD
  • Version Control (Git)
  • Debugger
  • Package manager
  • OS
  • Toolchain
  • Build System

這篇文章介紹了 Build System 的自動化流程,並討論了使用 Build System 的好處,以及 CMake 是如何生成 Build System 的。使用 CMake 可以讓整個編譯流程跨平台且更容易維護,並且可以自動化繁瑣的工作,讓開發者專注於寫程式碼。

C++ 的編譯過程

首先要從編譯說起。

事實上,我們在建構 C++ project 時,可以完全不使用 CMake 這類 build system,只是如此缺點是這樣的專案在建構時會很難維護,而且缺乏 scalibility。

為了說明 Build system 的好用之處,我們先看看如果不用 build system 的話,需要的步驟會多繁瑣。

Compiler 在把 source file (e.g. main.cpp) 轉換成 executable (e.g. a.out) 的過程中需要很多個步驟,但初學時通常用一個指令就足夠了,因為 compiler 都會幫你處理好所有的事情。

假設我們有個簡單的程式 main.cpp

#include <iostream>




int main() {
	std::cout << "hello c++" << std::endl;
}

可以使用 clang++ 編譯 main.cpp 這個 source file,同時產生了 executable main

$ clang++ -o main main.cpp

但在背後,clang++,以及所有其他的 compiler,都幫你做了這些事情:

<pre>   💡 Source file (`main.cpp`) → [ Preprocessor ] → preprocessed file (`main.ii`)   → [ Compiler ] → assembly file (`main.s`) → [ Assembler ] → object file   (`main.o`) → [ Linker ] (with some libraries if needed) → executable (`a.out`   or `main`) </pre>
Preprocess

在這個步驟會藉由 preprocessor 產生 preprocessed file。主要做的事情是 "#include expansion",就是把所有 #include 到的東西直接複製貼上到這個檔案裡面,然後會產生 translation unit。

要注意的是,我們需要把輸出寫道 main.ii 裡面,方便我們看之後的過程。

$ clang++ -E main.cpp > main.ii
Compilation

根據我們的 preprocessed file,這一步會產生很醜但還是勉強可以理解的 assembly code (main.s),內容會根據使用者的系統而不同,但通常我們不會需要去看裡面的內容。

這一步會自動產生 main.s,不需要 redirect output。

$ clang++ -S main.ii
Assembly

把 assembly code 轉成 machine code,然後會產生 object file main.o。到這裡 main.o 會是一個二進位檔案,是對人類完全沒有閱讀意義的檔案。

$ clang++ -c main.s
Linking

Linker 主要的作用是,把 function declaration 連結到它的 compiled implementation,也就是 object files。

這個步驟會 link 不同的 libraries (在這個例子中不需要),進而產生 executable file main

$ clang++ main.o -o main

(註:如果需要仔細看 clang++ 的使用說明,可以直接用 man

$ man clang++

這個例子很簡單,但他有一個前提是所有的 code 都寫在 main.cpp 裡面,而這顯然在現實中是不可能的。而且我們也沒有使用 Library。一般來說,除了 C++ standard libraries,如 <iostream><vector> 等,我們還會用到他人寫的以及自己寫的 library,而這些 library 通常會因為較好維護等的原因,分別放在多個檔案中 (*.hpp , *.cpp)。

什麼是 Library?

Library 就是已經寫好的 code,打包好了,其他人 (包括自己) 就能方便使用。

使用者在用 Library 時,會需要兩個東西:

  • Object file 。這是編譯好的 implementation。簡單來說就是程式的本體。   - 可以根據程式的本體在哪分成兩種:static library & dynamic library。   - 由 *.cpp 檔編譯而成。
  • Header file。這是告訴使用者這個程式要如何使用,就是廣義的 API 概念。   - 就是 *.hpp

記得 Linker 的功能嗎?

<aside> 💡 Linker maps the a function declaration (header file) to its compiled implementation (object file)。 </aside>

所以 library 跟 linker 的使用是分不開的。

這裡寫一個簡單的 library greet() 為例子說明。

greet() 的 function declaration 跟 definition 分成不同的檔案 (modules),並在 main.cpp 中呼叫。

greet.hpp - Declaration

#pragma once  // ensure this file is included only once
void greet();

greet.cpp - Definition

#include <iostream>
#include "greet.hpp"
void greet() { std::cout << "hello there" << std::endl; }

main.cpp - Calling from other program

#include "greet.hpp"
int main() {
	greet();
	return 0;
}

有了 source file,要建構 Library greet 需要下列幾個步驟:

Compile the modules

這步驟包含了前面提到的 preprocess, compilation, assembly,會產生 greet 這個 module 的 object file greet.o

$ clang++ -c greet.cpp
Create libraries archive

用打包工具 ar 以及參數 rcs,將 greet.o 打包成一個 library,命名為 libgreet.a。要注意 library 必須命名為lib<library name>.a,否則之後 linker 會抓不到。

這個例子只有將一個檔案 greet.o 打包,實際上可以將很多個檔案打包成一個 library。

$ ar rcs libgreet.a greet.o <other_modules_here>
Compile main application

像之前一樣產生 main.cpp 的 object file main.o

$ clang++ -c main.cpp
Link main application to libraries

有了主要程式 main.o 跟 library greet,就可以打包成最後的 executable main

  • -L . add current directory so library search path.
  • -lgreet 告訴 compiler 要找 greet 這個 library。所以前面的命名正確性很重要。
$ clang++ main.o -o main -L . -lgreet

到目前為止我們建立了一個 library greet,並且成功的連結到主要的程式 main 裡面。

但是對於所有的 libraries,我們都要跑過以上 4 個步驟,非常的麻煩。而且當 project 規模越來越大,要去 maintain 這一套流程是很困難的事。

所以我們需要 build system 來自動話這些工作。

什麼是 Build System?

Build system 就是一套自動化工具,讓你用一個 script 就可以自動化上述的建構流程,最好可以跨平台,並且容易閱讀且好維護。

一般來說,使用者可以很 hardcore 的用 shell script,或更好的 Makefile 去做這件事,但這篇文章要討論的是 CMake。

首先必須要理解的是,CMake 並不是一個 build system,而是一個 build system generator。

C++ 本身是跨平台的程式語言,但 C++ 的編譯工具卻不是。所以就要使用 CMake 這類的跨平台工具去產生對應到不同平台的編譯工具,並且達到易讀好維護的目的。

簡而言之,使用 CMake 會根據使用者的開發系統產生一個 build system (如在 linux & macOS 上是 Makefile),然後我們再使用這個 build system 去實際建構出程式。

重新看一次前一個例子的所有指令:

[1] clang++ -c greet.cpp
[2] ar rcs libgreet.a greet.o <other_modules_here>
[3] clang++ -c main.cpp
[4] clang++ main.o -o main -L . -lgreet

CMakeLists.txt 對應的寫法:

add_library(greet greet.cpp)        # [1] and [2]
add_executable(main main.cpp)       # [3]
target_link_libraries(main greet)   # [4]

顯而易見,CMake 大大簡化了手動建構系統冗長的編譯指令。

所有 CMake 有關的 build file 都放在 ./build 裡面,這樣可以保持 project folder 的乾淨,就算 build 過程出了問題,整個 build folder 刪掉重來都沒問題。

這裡也用一個簡單的 CMakeLists.txt 說明。

簡單的 CMake example

延續上面的範例,並且加上 CMakeLists.txt

<project_name>
	├── CMakeLists.txt
	├── build/
	│   └── ... cmake generated files go here
	├── greet.cpp
	├── greet.hpp
	└── main.cpp

CMakeLists.txt 內容如下:

cmake_minimum_required(VERSION 3.15)
project(my_project_name)




add_library(greet greet.cpp)
add_executable(main main.cpp)
target_link_libraries(main greet)

使用 CMake 的流程如下:

[0] $ cd <project directory>




# Generates a Project Buildsystem with current dir as CMake project root dir
[1] $ cmake -S . -B build




# builds a project, keeps cmake files in build folder
[2] $ cmake --build build




[3] $ make               # build using Make

[1] ~ [3] 是使用 CMake 產生一個 makefile,然後 [4] 再使用 make 把目標實際建構出來。

使用 CMake 的典型 Project 架構

當然還需要 git 等工具,不過這邊先不討論。

<project_name>
	├── CMakeLists.txt
	├── README.md
	├── build                # keep CMake files here
	├── include              # other libraries
	│   └── project_name
	│       └── some_library.hpp
	├── results              # build result
	│   ├── bin
	│   │   └── main
	│   └── lib
	│       └── libtools.a
	├── src                   # source files
 	│   ├── CMakeLists.txt
	│   └── project_name
	│       ├── CMakeLists.txt
	│       ├── main.cpp
	│       ├── tools.cpp
	│       └── tools.hpp
	└── tests                  # testing tools
    ├── CMakeLists.txt
    └── tests.cpp
Reference

https://chchwy.github.io/2021/08/cmake-basics/

https://cliutils.gitlab.io/modern-cmake/

https://www.ipb.uni-bonn.de/html/teaching/modern-cpp-2021/slides/lecture_1.pdf

https://ithelp.ithome.com.tw/articles/10221101

https://gitlab.kitware.com/cmake/community/-/wikis/FAQ#how-do-i-use-a-different-compiler

https://stackoverflow.com/questions/45933732/how-to-specify-a-compiler-in-cmake

https://www.youtube.com/watch?v=18c3MTX0PK0&list=PLlrATfBNZ98dudnM48yfGUldqGD0S4FFb

commands to build a project