開發 C++ 會用到很多工具,除了基本的編輯器 Text Editor 跟編譯器 Compiler,還有:
這篇文章介紹了 Build System 的自動化流程,並討論了使用 Build System 的好處,以及 CMake 是如何生成 Build System 的。使用 CMake 可以讓整個編譯流程跨平台且更容易維護,並且可以自動化繁瑣的工作,讓開發者專注於寫程式碼。
首先要從編譯說起。
事實上,我們在建構 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>在這個步驟會藉由 preprocessor 產生 preprocessed file。主要做的事情是 "#include expansion",就是把所有 #include
到的東西直接複製貼上到這個檔案裡面,然後會產生 translation unit。
要注意的是,我們需要把輸出寫道 main.ii
裡面,方便我們看之後的過程。
$ clang++ -E main.cpp > main.ii
根據我們的 preprocessed file,這一步會產生很醜但還是勉強可以理解的 assembly code (main.s
),內容會根據使用者的系統而不同,但通常我們不會需要去看裡面的內容。
這一步會自動產生 main.s
,不需要 redirect output。
$ clang++ -S main.ii
把 assembly code 轉成 machine code,然後會產生 object file main.o
。到這裡 main.o
會是一個二進位檔案,是對人類完全沒有閱讀意義的檔案。
$ clang++ -c main.s
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 就是已經寫好的 code,打包好了,其他人 (包括自己) 就能方便使用。
使用者在用 Library 時,會需要兩個東西:
*.cpp
檔編譯而成。*.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
需要下列幾個步驟:
這步驟包含了前面提到的 preprocess, compilation, assembly,會產生 greet 這個 module 的 object file greet.o
。
$ clang++ -c greet.cpp
用打包工具 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>
像之前一樣產生 main.cpp
的 object file main.o
。
$ clang++ -c main.cpp
有了主要程式 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 就是一套自動化工具,讓你用一個 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
說明。
延續上面的範例,並且加上 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 把目標實際建構出來。
當然還需要 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
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