iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Software Development

30 天 CMake 跨平台之旅系列 第 11

[Day 11] Library 類型

  • 分享至 

  • xImage
  •  

本日內容

  • Static Libraries
  • Shared Libraries
  • Object Libraries
  • Interface Libraries
  • Link Seams
  • 預告

連結: Day 11 - Colab

先來回憶一下 Day 8 在介紹 Target 類型時, 提到的幾種 Library 類型

  • Normal Libraries
    • Shared Libraries
    • Static Libraries
    • Module Libraries
  • Object Libraries
  • Imported Libraries
  • Alias Libraries

今天會介紹 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

來複習一下 normal libraries 的指令

add_library(targetName
  [STATIC | SHARED | MODULE]
  [EXCLUDE_FROM_ALL]
  source1 [source2 ...]
)

好習慣是 要設 keyword, 否則 CMake 會從 direcotry-scope 的 BUILD_SHARED_LIBS 決定 library type (預設為 STATIC)
有點危險

Static Libraries

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
)

Shared Libraries

相較於上面的 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
)

Object Libraries

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
)

Module Libraries

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

Interface Libraries

add_library(<name> INTERFACE)

Interface libraries 本身不會被 compiler, 他是 CMake 中 library 的抽象, 單純用來表明 targets 間的依賴關係, interface libraries 的 properties 是可以被 link 他的 targets 繼承的!

通常會用在 header-only 的 library, 或是當作對外的介面, 把實作隱藏起來, 其中一個例子就是 Link Seams!

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 了!


上一篇
[Day 10] Build Basics
下一篇
[Day 12] 寫出我的第一個 Library
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言