連結: Day 11 - Colab
先來回憶一下 Day 8 在介紹 Target 類型時, 提到的幾種 Library 類型
今天會介紹 Normal Libraries 和 Object Libraries, 並額外介紹很重要的 Interface Libraries 和 Link Seams
Imported Libraries 和 Alias Libraries 會留到 Day 14 再介紹
為了方便說明, 我們會依照今天介紹的幾種 libraries 分類, 所以專案架構會長這樣 (也可以直接看 Colab)
cmake-example/
├─ src/
│ ├─ lib/
│ │ ├─ object/
│ │ │ ├─ object.cpp
│ │ │ ├─ CMakeLists.txt
│ │ ├─ module/
│ │ │ ├─ module.cpp
│ │ │ ├─ CMakeLists.txt
│ │ ├─ shared/
│ │ │ ├─ shared.cpp
│ │ │ ├─ CMakeLists.txt
│ │ ├─ static/
│ │ │ ├─ static.cpp
│ │ │ ├─ CMakeLists.txt
│ │ ├─ CMakeLists.txt
│ ├─ include/
│ │ ├─ interface/
│ │ │ ├─ interface.h
│ │ ├─ object/
│ │ │ ├─ object.h
│ │ ├─ module/
│ │ │ ├─ module.h
│ │ ├─ static/
│ │ │ ├─ static.h
│ │ ├─ shared/
│ │ │ ├─ shared.h
│ ├─ main.cpp
│ ├─ CMakeLists.txt
├─ CMakeLists.txt
來複習一下 normal libraries 的指令
add_library(targetName
[STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...]
)
好習慣是 要設 keyword, 否則 CMake 會從 direcotry-scope 的 BUILD_SHARED_LIBS
決定 library type (預設為 STATIC
)
有點危險
Static library 是 object files (*.o
) 的檔案集合, 這些 object files 都是 compile 過的 binaries, 每個 object file 都會包含 reference symbols, 比如 functions, global variables, static variables 等等
Compiler 在 build 的過程中會把 Archiver (ex. ar
) 叫起來打包這些 object files, 最終變成 .a
檔, 在 CMake 中只要寫 target name 就好, CMake 會幫我們在 link 該 library 的時候將 target name 轉成真正的檔名, 比如 libMyApp_Static.a
打包後, 當 executables 或是 shared libraries 在 link 他時, compiler 會去 static library 拿需要的 symbols, 然後塞到 executables 或是 shared libraries 的檔案中
所以, link static library 的 targets 最終產出檔案大小都會變大, 因為所有需要的 object files 都被塞到這些 binaries, 這就是 STATIC
的意思, 他在 compile-time 就被塞到最後的 binaries 了
好處是我們在執行期間就不需要他們了, 所以也不需要安裝
src/lib/static/CMakeLists.txt
add_library(MyApp_Static STATIC
static.cpp
)
相較於上面的 static libraries 在 link 時會把 symbols 都塞到 link 他的 binaries 裡面, shared library 則是在 binaries 執行時才 動態 去 .dynsym
找對應的 symbols 執行, 所以不同的 binaries 可以 共用 同一個 shared library, 就省了很多空間!
我們可以用 readelf
或是 nm
來看 shared library 或 module library 的 symbols 有哪些, 很好用!
Note: readelf
可以看到的東西很多, 除了 .symtab
和 .dynsym
以外, 還包含了整個 .so
的結構, 比如 sections, symbols, section headers, program headers, 入口點等等, 所以他其實比較適合用來查看整個 binary 底層結構nm
就比較單純一點, 只專注查看各種 symbols 的名稱, address, 類型等信息, 會比較好看
src/lib/shared/CMakeLists.txt
add_library(MyApp_Shared SHARED
shared.cpp
)
add_library(<name> OBJECT [<source>...])
就像上面在介紹 static libraries 時提過的, archiver 會把 object files 打包成 libXXX.a
檔, 讓 link 他 binaries 能夠找到對應的 symbols
但是 object libraries 就只會 build 出一堆 .o
檔, 並沒有被 archiver 或是 linker 處理過, 所以他其實 並不是 library
如果 targets 想要 link 他, 需要用 generator expression, 比如$<TARGET_OBJECTS:MyApp_Object>
建議還是使用 static library 就好, 這邊僅為示範用途
src/lib/object/CMakeLists.txt
add_library(MyApp_Object OBJECT
object.cpp
)
src/lib/module/CMakeLists.txt
add_library(MyApp_Module MODULE
module.cpp
)
也會 build 出 .so
檔, 但和 shared libraries 不同的是, 他是程式的 plugin, 所以不會被 link, 可以用 dlopen
讀檔拿到 handle 後, 用 dlsym
取得 module library 的 symbols
就可以在 runtime 使用, 詳見今日的 Colab
add_library(<name> INTERFACE)
Interface libraries 本身不會被 compiler, 他是 CMake 中 library 的抽象, 單純用來表明 targets 間的依賴關係, interface libraries 的 properties 是可以被 link 他的 targets 繼承的!
通常會用在 header-only 的 library, 或是當作對外的介面, 把實作隱藏起來, 其中一個例子就是 Link Seams!
最後, 我們來講講什麼是 Link Seams
看到這裡我們可以知道, CMake 的 targets 就是一個方便我們管理各個 targets 間依賴關係的抽象!
CMake 在發現 target A 需要 target B 的時候, 就會優先去 build target B, 這也是 CMake 3.0 target-centered model 的核心概念: 由每個 targets 自己決定要怎麼 build, 這在 Day 8 有詳細介紹
這些 targets 間的依賴關係, 從最開始的 target 一路 link 到最後的 libraries, 就可以畫出 dependency tree
比如我們寫一個 Exe1 executable, 他會用到 Middle library, Middle library 會用到 Seam library
所以 dependency tree 可以看成是: Exe1 <- Middle <- Seam
目前都很美好, 我們 build 出了 Exe1 executable 並且也能順利執行
現在我們想對 Middle 進行 unit test, 所以另外寫了一個 test binary TestExe1, 並 link Middle library, 再寫一個 SeamStub 讓 Middle link
然後就會發現...我們很難把 Seam mock 掉, 比較直覺的想法可能是根據 build type (見 Day 6) 決定是否要 link, 比如 build debug 版的時候改讓 Middle link SeamStub
這樣測試的問題算是解決了, 但是如果未來有更多要用到 Middle library 的 executables Exe#, 且各自有實作不同的 Seam library 邏輯, 那麼用 build type 判斷的方法就不適用了
所以! 這種情況下最好將 Seam 的介面抽象出來, 將實作留給 dependency tree 頂端的 binary 決定要實作要用誰
這是什麼意思呢?
如果對 Object-Oriented Programming 有概念的人, 就會發現, 咦? 這不就是 dependency injection (DI) 嗎?
沒錯! 這其實就是 library 層面的 DI
如果不知道 DI 是什麼, 以下簡單解釋
假設我們像上面一樣有三層 dependency, 我們可以將最後的 library (ex. Seam) 改成沒有實作的 SeamIface 和實作他的 SeamImpl, Middle 改成 link SeamIface, SeamImpl 留給頂端的 executable (ex. Exe1) link, 這樣就會變成由 executable 提供 SeamIface 的實作!
如此一來, Middle 都是 link 到 SeamIface, 但是卻可以有無數個實作!
比如當需要測試時, 我們只需要讓 executable 改成 link 測試的 library (ex. SeamStub) 即可 (前提是該測試的 library 有實作 SeamIface), 我們就不需要再修改 Middle 後 link 的邏輯了! 讚讚👍👍👍
比較麻煩的是, 我們有多少個 Exe# 就需要寫多少次 target_link_libraries(Exe# ... SeamImpl)
所以, 我們可以設定 SeamImpl 的 INTERFACE_LINK_LIBRARIES_DIRECT
target property 來解決! 設定該 property 的 target, 會將該 library 的 object files 一路傳給 link dependency 上層的 所有 libraries
你可能會想, 為何不讓 Middle 直接 link SeamImpl 就好, 不是一樣的意思嗎?
沒錯, 但是因為 Middle 本身 不需要 link SeamImpl, 設定 INTERFACE_LINK_LIBRARIES_DIRECT
的 target 本身並不會 link 該 library, 這樣 Middle library 的大小也能小一點
Link Seams 的範例 code 會在明天提供
了解了這些概念後, 接下來就可以來寫我們的 library 了!