前言
作為講解 C++ package 使用觀念的首篇,我們要先來解答一個普羅大眾的疑惑:
為什麼 C++ 的 package management 會這麼困難
其衍伸問題包括但不限於:
- 為什麼 C++ 沒有像 npm, pip, maven 一樣可以寫寫 config 下個指令就能使用 OpenCV 的辦法?
- 為什麼我感覺每個抓下來的 package 都要 Make CMake 整個重新 compile 才能用?
- 抓到 binary release 了怎麼好像不太能用?
- 為什麼我用個 library 還要綁 compile,為什麼就沒聽說過別的語言 package 會綁 interpreter, compiler
- library 不就是 API 嗎?
以上這些問題其實都關連一個很重要的東西叫做linker
但是要講 linker 不免俗就要帶到系統程式等等相關的知識等非常八股的東西,而且 platform dependent
所以我決定直接從最好理解的環節切入: ABI
但這樣一來必然損失某些細節,所以有問題的人就在你看到這邊文章的社群下面留言發問吧。
ABI (Application binary interface)
大部分的人應該都對 API 很熟悉
import(include) module 然後 call function, 就是一個基本的 API call
以 javascript, python, 這類語言來說
interpreter 讀到原始碼中的 API call 之後,帶著 API name 去目標模組找到 API 的定義之後就能夠執行。
而對於 C/C++ 這類設計目的以編譯後即為 machine code 的語言來說 function call 是怎麼完成的?
不是就 call/jump address 嗎?
學過 assembly 的同學可能馬上就會想到 machine code 指令裡就有 call/jump 到某個 address 的指令
那麼很顯然 compiler 要做的事情就是把各種 function name 對應到 function 定義的 address 就好了。
對嗎?
我把兩個 .c 拆開編譯然後互相 call 對方的 function
想像一個 .c 檔的內容大概是這樣:
int foo();
int main() {
foo();
}
顯然,上個段落介紹的方法並不能處理被呼叫的 function 定義不在 compiling 當下處理的原始碼檔的情況。
沒有定義的話自然就沒有 address 可以直接 call
如上面的 example, compiler 如果單純編譯這份 code, foo 的 address 對於 compiler 來說就是未知的
那麼分開編譯的兩份 binary code 是透過什麼聯結對方的 function 定義呢?
在二進位檔中的 Name, ABI
我們還是靠一個字串構成的 Name 去尋找另一個二進位檔中對應 Name 的定義。
更具體一點來說,以 Linux 的 ELF format 來看,
這種二進位檔叫做 relocatable ELF(也就是 .o 檔),
當中會帶有一份 symbol table 標記哪些 symbol 必須從"外部" (external)或本地取得。
這個 Name, Symbol 就叫做 ABI。
這個過程中
- machine code 是 CPU dependent
- ELF 是 OS dependent
那麼回到我們的主題,我們想要 call library
所以在同樣 OS, CPU 上生成的 binary 就能互相呼叫囉?
很抱歉,C++ 的 binary 有 Compiler dependency
接下來就是要談談寫 C++ 最讓人不解的問題:
明明 CPU, OS 都確認過了,為什麼 library 還是不能用?
事情就是這麼的令人絕望,你下載了某個 library 的 binary release
上面寫著 win32 x86_64
你確實是 windows, 也是 x86 64 bits, 你花了大把時間 debug 你的編譯指令
然而就是不能用。
C++ name mangling
有別於 C, C++ 是一個有 class, overloading, template 的高級語言。
試想:兩個 function 使用相同名稱時,compiler 要怎麼編譯才能區分他們的 ABI 呢?
你應該也想到了把型別放進名稱對吧?
大家都是這麼想的,但是要怎麼實作呢?
A 說我們用一個底線隔開如:foo_int
B 說不用我們直接用 i 接在後面 fooi
C 說我們再加點編碼讓他不會太容易跟別的 user defined 撞名
...
所以當你編譯 C++ 程式碼看到 "undefined reference" 時你會看到像是 _Z3fooi
這種 reference 肯定不是你寫的 function name
因為它其實是被 compiler 加工過的東西。
這件事沒有一個準則,C++ standard 沒有規範
雖然各個平台皆有試圖訂定一些標準,但這畢竟不是 language standard
天下尚未統一,compiler 怎麼做都不犯規,於是 C++ 編譯生成的 binary 就存在 Compiler dependency
最糟糕的情況是,有時候相同 compiler 不同版本也是不能相通的。
這個問題也間接影響到其他語言要跟C++溝通的情況,比如 Java JNI, node NAN, Python C++ binding.
What about C?
C語言是一個非常特殊的語言,由於它的歷史地位和定位。
他的 ABI 規則在大部分 compiler 上都是一致的,
也就是說他是真的只需要看 OS 和 CPU 就幾乎 binary 能通用的語言。
我知道看到這裡有些敏感的人會意識到 MinGW
所以我這裡補充一句,.a .o 不是 windows 上的 library file format。
解決方案
大部分的 R&D, 軟體工程師大概都不想也不會特地花很多時間在整治開發環境上。
裝個 library 還要研究一堆環境相依性問題這是最令人恐懼的。
所以對一部分 C++ programmer 來說,簡單的元件就自己寫吧!
然後就給人 C++ 開發者喜歡造輪子的印象
然而當你看完這篇文章就了解
真相不是這麼回事,一切都是不得已的
這裡就粗略的講幾種思路,之後的文章會再寫目前筆者本人覺得最好的做法。
用 C++ 寫 code, 用 C 溝通
簡單來說,就是善用 extern "C"。
既然 C 有特權,我們就充分利用他
然而你無法限制他人,你仍然會遇到 C++ package
找一個什麼 compiler/CPU/OS 都支援的 package manager
這實在太困難了,Vcpkg 也許最接近這個要求但肯定沒有完全足夠。
總是從 source code 編譯 package
同樣是痛苦的道路,但是如果有 package manager 能夠幫我們做完這件事,
那也許是相對輕鬆的,後續的文章(如果有的話)
將會以這種路線介紹一個本人過去常用的方案。
Summary
本篇文章盡可能用淺白的方式介紹了 C++ library linking 的一種困境和從原理層面探討其原因。
希望對正在 debug 開發環境的你有所幫助
以上
2021/7/19 author by John Chang