[重新理解C++] Package 觀念: ABI 和 API


前言

作為講解 C++ package 使用觀念的首篇,我們要先來解答一個普羅大眾的疑惑:

為什麼 C++ 的 package management 會這麼困難

其衍伸問題包括但不限於:

  1. 為什麼 C++ 沒有像 npm, pip, maven 一樣可以寫寫 config 下個指令就能使用 OpenCV 的辦法?
  2. 為什麼我感覺每個抓下來的 package 都要 Make CMake 整個重新 compile 才能用?
  3. 抓到 binary release 了怎麼好像不太能用?
  4. 為什麼我用個 library 還要綁 compile,為什麼就沒聽說過別的語言 package 會綁 interpreter, compiler
  5. 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。

這個過程中

  1. machine code 是 CPU dependent
  2. 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

#C #C++ #軟體工程 #套件管理 #software engineering #Package Manager







你可能感興趣的文章

如何完成程式設計共學營報名流程?

如何完成程式設計共學營報名流程?

Day 8 - Function & Caesar Cipher

Day 8 - Function & Caesar Cipher

React 基礎:先別急著學 React

React 基礎:先別急著學 React






留言討論