https://zhangrunnan.com/cpp-binary-compatibility/
https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B
在深入了解前,首先我們要明確兩個概念:二進制兼容和源碼兼容。
-
二進制兼容:在升級庫文件的時候,不必重新編譯使用此庫的可執行文件或其他庫文件,並且程序的功能不被破壞。
-
源碼兼容:在升級庫文件的時候,不必修改使用此庫的可執行文件或其他庫文件的源代碼,只需重新編譯應用程序,即可使程序的功能不被破壞。
主要是動態庫存在二進制的兼容問題,其中又主要是c++的虛函數作為接口會產生問題
https://blog.csdn.net/Solstice/article/details/6244905
比如現在 Graphics 庫發布了 1.1.0 和 1.2.0 兩個版本,這兩個版本可以不必是二進制兼容。
用戶的代碼從 1.1.0 升級到 1.2.0 的時候要重新編譯一下,反正他們要用新功能都是要重新編譯代碼的。
如果要原地打補丁,那么 1.1.1 應該和 1.1.0 二進制兼容,而 1.2.1 應該和 1.2.0 兼容。
如果要加入新的功能,而新的功能與 1.2.0 不兼容,那么應該發布到 1.3.0 版本。
對於用戶可見的部分,升級時要注意二進制兼容性,選用合理的版本號;對於用戶不可見的部分,在升級庫的時候就不必在意。
————————————————
ABI和API
應用二進制接口(application binary interface,縮寫為 ABI)描述了應用程序(或者其他類型)和操作系統之間或其他應用程序的低級接口。
ABI涵蓋了各種細節,如:數據類型的大小、布局和對齊;調用約定等。
在了解二進制兼容和源碼兼容兩個定義以后,我們再看與其類似且對應的兩個概念:ABI和API。ABI不同於API(應用程序接口),API定義了源代碼和庫之間的接口,因此同樣的代碼可以在支持這個API的任何系統中編譯,然而ABI允許編譯好的目標代碼在使用兼容ABI的系統中無需改動就能運行。
舉個例子,在Qt和Java兩種跨平台程序中,API像是Qt的接口,Qt有着通用接口,源代碼只需要在支持Qt的環境下編譯即可。ABI更像是Jvm,只要支持Jvm的系統上,都可以運行已有的Java程序。
C++的ABI
ABI更像是一個產品的使用說明書,同理C++的ABI就是如何使用C++生成可執行程序的一張說明書。編譯器會根據這個說明書,生成二進制代碼。C++的ABI在不同的編譯器下會略有不同。
C++ABI的部分內容舉例:
- 函數參數傳遞的方式,比如 x86-64 用寄存器來傳函數的前 4 個整數參數
- 虛函數的調用方式,通常是 vptr/vtbl 然后用 vtbl[offset] 來調用
- struct 和 class 的內存布局,通過偏移量來訪問數據成員
綜上所述,如果可執行程序通過以上說明書訪問動態鏈接庫A,以及此庫的升級版本A+,若按此說明書上的方法,可以無痛的使用A和A+,那么我們就稱庫A的這次升級是二進制兼容的。
破壞二進制兼容的幾種常見方式 【改變了虛函數的offset或者成員的順序】
- 添加新的虛函數
- 不導出或者移除一個導出類
- 改變類的繼承
- 改變虛函數聲明時的順序(偏移量改變,導致調用失敗)
- 添加新的非靜態成員變量(類的內存布局改變,偏移量也發生變化)
- 改變非靜態成員變量的聲明順序
不會破壞二進制兼容的幾種常見方式
- 添加非虛函數(包括構造函數)
- 添加新的類
- 添加Qt中的信號槽
- 在已存在的枚舉類型中添加一個枚舉值
- 添加新的靜態成員變量
- 修改成員變量名稱(偏移量未改變)
- 添加
Q_OBJECT,Q_PROPERTY,Q_ENUMS,Q_FLAGS宏,添加這些宏都是修改了moc生成的文件,而不是類本身
只要我們知道了程序是以什么方式訪問動態庫的(C++的ABI),那么我們就很好判斷,哪些操作會破壞二進制兼容。更多方式請參見Policies/Binary Compatibility Issues With C++
解決二進制兼容問題的相關方法
- 使用
Bitflags即位域
|
1
2
3
|
uint m1 :
1;
uint m2 :
3;
uint m3 :
1;
|
|
1
2
3
4
|
uint m1 :
1;
uint m2 :
3;
uint m3 :
1;
uint m4 :
2; // new member without breaking binary compatibility.
|
- 使用
PImpl機制,詳情參見PImpl機制以及Qt的D-Pointer實現 - 使用靜態庫(當然也隨之帶來一系列弊端)
C++抽象類和Java的接口
讀到這里大家也許會奇怪,作者是不是放錯地方了?其實不然,只是在我們了解二進制兼容后,可以更好地理解這組概念。之前我一直認為C++的抽象類就類似於Java的接口,現在發現,如果把一個C++的抽象類作為動態庫的接口發布,那將是毀滅的。因為你無法增加虛函數,無法增加成員變量,這使得這個接口變得非常的不友好。這也就是Java接口的優勢所在。Java 實際上把 C/C++ 的 linking 這一步驟推遲到 class loading 的時候來做,便不存在上述二進制兼容的問題。
理解Java二進制兼容的關鍵是要理解延遲綁定(Late Binding)。延遲綁定是指Java直到運行時才檢查類、域、方法的名稱,而不象C/C++的編譯器那樣在編譯期間就清除了類、域、方法的名稱,代之以偏移量數值——這是Java二進制兼容得以發揮作用的關鍵。
由於采用了延遲綁定技術, 方法、域、類的名稱直到運行時才解析,意味着只要域、方法等的名稱(以及類型)一樣,類的主體可以任意替換。
參考
】
https://blog.csdn.net/Solstice/article/details/6233478
本文主要討論 Linux x86/x86-64 平台,偶爾會舉 Windows 作為反面教材。
C/C++ 的二進制兼容性 (binary compatibility) 有多重含義,本文主要在“頭文件和庫文件分別升級,可執行文件是否受影響”這個意義下討論,我稱之為 library (主要是 shared library,即動態鏈接庫)的 ABI (application binary interface)。
至於編譯器與操作系統的 ABI 留給下一篇談 C++ 標准與實踐的文章。
- 什么是二進制兼容性
在解釋這個定義之前,先看看 Unix/C 語言的一個歷史問題:open() 的 flags 參數的取值。open(2) 函數的原型是
int open(const char *pathname, int flags);
其中 flags 的取值有三個: O_RDONLY, O_WRONLY, O_RDWR。
與一般人的直覺相反,這幾個值不是按位或 (bitwise-OR) 的關系,即 O_RDONLY | O_WRONLY != O_RDWR。如果你想以讀寫方式打開文件,必須用 O_RDWR,而不能用 (O_RDONLY | O_WRONLY)。為什么?因為 O_RDONLY, O_WRONLY, O_RDWR 的值分別是 0, 1, 2。它們不滿足按位或 。
那么為什么 C 語言從誕生到現在一直沒有糾正這個不足之處?比方說把 O_RDONLY, O_WRONLY, O_RDWR 分別定義為 1, 2, 3,這樣 O_RDONLY | O_WRONLY == O_RDWR,符合直覺。而且這三個值都是宏定義,也不需要修改現有的源代碼,只需要改改系統的頭文件就行了。
因為這么做會破壞二進制兼容性。對於已經編譯好的可執行文件,它調用 open(2) 的參數是寫死的,更改頭文件並不能影響已經編譯好的可執行文件。比方說這個可執行文件會調用 open(path, 1) 來寫 文件,而在新規定中,這表示讀 文件,程序就錯亂了。
以上這個例子說明,如果以 shared library 方式提供函數庫,那么頭文件和庫文件不能輕易修改,否則容易破壞已有的二進制可執行文件,或者其他用到這個 shared library 的 library。
操作系統的 system call 可以看成 Kernel 與 User space 的 interface,kernel 在這個意義下也可以當成 shared library,你可以把內核從 2.6.30 升級到 2.6.35,而不需要重新編譯所有用戶態的程序。
所謂“二進制兼容性”指的就是在升級(也可能是 bug fix)庫文件的時候,不必重新編譯使用這個庫的可執行文件或使用這個庫的其他庫文件,程序的功能不被破壞。
見 QT FAQ 的有關條款:http://developer.qt.nokia.com/faq/answer/you_frequently_say_that_you_cannot_add_this_or_that_feature_because_it_woul
在 Windows 下有惡名叫 DLL Hell,比如 MFC 有一堆 DLL,mfc40.dll, mfc42.dll, mfc71.dll, mfc80.dll, mfc90.dll,這是動態鏈接庫的本質問題,怪不到 MFC 頭上。
- 有哪些情況會破壞庫的 ABI
到底如何判斷一個改動是不是二進制兼容呢?這跟 C++ 的實現方式直接相關,雖然 C++ 標准沒有規定 C++ 的 ABI,但是幾乎所有主流平台都有明文或事實上的 ABI 標准。比方說 ARM 有 EABI,Intel Itanium 有 http://www.codesourcery.com/public/cxx-abi/abi.html ,x86-64 有仿 Itanium 的 ABI,SPARC 和 MIPS 也都有明文規定的 ABI,等等。x86 是個例外,它只有事實上的 ABI,比如 Windows 就是 Visual C++,Linux 是 G++(G++ 的 ABI 還有多個版本,目前最新的是 G++ 3.4 的版本),Intel 的 C++ 編譯器也得按照 Visual C++ 或 G++ 的 ABI 來生成代碼,否則就不能與系統其它部件兼容。
C++ ABI 的主要內容:
函數參數傳遞的方式,比如 x86-64 用寄存器來傳函數的前 4 個整數參數
虛函數的調用方式,通常是 vptr/vtbl 然后用 vtbl[offset] 來調用
struct 和 class 的內存布局,通過偏移量來訪問數據成員
name mangling
RTTI 和異常處理的實現(以下本文不考慮異常處理)
C/C++ 通過頭文件暴露出動態庫的使用方法,這個“使用方法”主要是給編譯器看的,編譯器會據此生成二進制代碼,然后在運行的時候通過裝載器(loader)把可執行文件和動態庫綁到一起。如何判斷一個改動是不是二進制兼容,主要就是看頭文件暴露的這份“使用說明”能否與新版本的動態庫的實際使用方法兼容。因為新的庫必然有新的頭文件,但是現有的二進制可執行文件還是按舊的頭文件來調用動態庫。
- 這里舉一些源代碼兼容但是二進制代碼不兼容例子
給函數增加默認參數,現有的可執行文件無法傳這個額外的參數。
增加虛函數,會造成 vtbl 里的排列變化。(不要考慮“只在末尾增加”這種取巧行為,因為你的 class 可能已被繼承。)
增加默認模板類型參數,比方說 Foo 改為 Foo >,這會改變 name mangling
改變 enum 的值,把 enum Color { Red = 3 }; 改為 Red = 4。這會造成錯位。當然,由於 enum 自動排列取值,添加 enum 項也是不安全的,除非是在末尾添加。
給 class Bar 增加數據成員,造成 sizeof(Bar) 變大,以及內部數據成員的 offset 變化,這是不是安全的?通常不是安全的,但也有例外。
如果客戶代碼里有 new Bar,那么肯定不安全,因為 new 的字節數不夠裝下新 Bar。相反,如果 library 通過 factory 返回 Bar* (並通過 factory 來銷毀對象)或者直接返回 shared_ptr,客戶端不需要用到 sizeof(Bar),那么可能是安全的。
同樣的道理,直接定義 Bar bar; 對象(無論是函數局部對象還是作為其他 class 的成員)也有二進制兼容問題。
如果客戶代碼里有 Bar* pBar; pBar->memberA = xx;,那么肯定不安全,因為 memberA 的新 Bar 的偏移可能會變。相反,如果只通過成員函數來訪問對象的數據成員,客戶端不需要用到 data member 的 offsets,那么可能是安全的。
如果客戶調用 pBar->setMemberA(xx); 而 Bar::setMemberA() 是個 inline function,那么肯定不安全,因為偏移量已經被 inline 到客戶的二進制代碼里了。如果 setMemberA() 是 outline function,其實現位於 shared library 中,會隨着 Bar 的更新而更新,那么可能是安全的。
那么只使用 header-only 的庫文件是不是安全呢?不一定。如果你的程序用了 boost 1.36.0,而你依賴的某個 library 在編譯的時候用的是 1.33.1,那么你的程序和這個 library 就不能正常工作。因為 1.36.0 和 1.33.1 的 boost::function 的模板參數類型的個數不一樣,其中一個多了 allocator。
這里有一份黑名單,列在這里的肯定是二級制不兼容,沒有列出的也可能二進制不兼容,見 KDE 的文檔:http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B
- 哪些做法多半是安全的
前面我說“不能輕易修改”,暗示有些改動多半是安全的,這里有一份白名單,歡迎添加更多內容。
只要庫改動不影響現有的可執行文件的二進制代碼的正確性,那么就是安全的,我們可以先部署新的庫,讓現有的二進制程序受益。
增加新的 class
增加 non-virtual 成員函數
修改數據成員的名稱,因為生產的二進制代碼是按偏移量來訪問的,當然,這會造成源碼級的不兼容。
還有很多,不一一列舉了。
歡迎補充
- 反面教材:COM
在 C++ 中以虛函數作為接口基本上就跟二進制兼容性說拜拜了。具體地說,以只包含虛函數的 class (稱為 interface class)作為程序庫的接口,這樣的接口是僵硬的,一旦發布,無法修改。
比方說 M$ 的 COM,其 DirectX 和 MSXML 都以 COM 組件方式發布,我們來看看它的帶版本接口 (versioned interfaces):
IDirect3D7, IDirect3D8, IDirect3D9, ID3D10*, ID3D11*
IXMLDOMDocument, IXMLDOMDocument2, IXMLDOMDocument3
話句話說,每次發布新版本都引入新的 interface class,而不是在現有的 interface 上做擴充。這樣不能兼容現有的代碼,強迫客戶端代碼也要改寫。
回過頭來看看 C 語言,C/Posix 這些年逐漸加入了很多新函數,同時,現有的代碼不用修改也能運行得很好。如果要用這些新函數,直接用就行了,也基本不會修改已有的代碼。相反,COM 里邊要想用 IXMLDOMDocument3 的功能,就得把現有的代碼從 IXMLDOMDocument 全部升級到 IXMLDOMDocument3,很諷刺吧。
tip:如果遇到鼓吹在 C++ 里使用面向接口編程的人,可以拿二進制兼容性考考他。
- 解決辦法
- 采用靜態鏈接
這個是王道。在分布式系統這,采用靜態鏈接也帶來部署上的好處,只要把可執行文件放到機器上就行運行,不用考慮它依賴的 libraries。目前 muduo 就是采用靜態鏈接。
通過動態庫的版本管理來控制兼容性
這需要非常小心檢查每次改動的二進制兼容性並做好發布計划,比如 1.0.x 系列做到二進制兼容,1.1.x 系列做到二進制兼容,而 1.0.x 和 1.1.x 二進制不兼容。《程序員的自我修養》里邊講過 .so 文件的命名與二進制兼容性相關的話題,值得一讀。
- 用 pimpl 技法,編譯器防火牆
在頭文件中只暴露 non-virtual 接口,並且 class 的大小固定為 sizeof(Impl*),這樣可以隨意更新庫文件而不影響可執行文件。當然,這么做有多了一道間接性,可能有一定的性能損失。見 Exceptional C++ 有關條款和 C++ Coding Standards 101.
- Java 是如何應對的
Java 實際上把 C/C++ 的 linking 這一步驟推遲到 class loading 的時候來做。就不存在“不能增加虛函數”,“不能修改 data member” 等問題。在 Java 里邊用面向 interface 編程遠比 C++ 更通用和自然,也沒有上面提到的“僵硬的接口”問題。
(待續)
https://blog.csdn.net/Solstice/article/details/6244905
以虛函數作為接口在二進制兼容性方面有本質困難:“一旦發布,不能修改”。
假如我需要給 Graphics 增加幾個繪圖函數,同時保持二進制兼容性。這幾個新函數的坐標以浮點數表示,我理想中的新接口是:
--- old/graphics.h 2011-03-12 13:12:44.000000000 +0800
+++ new/graphics.h 2011-03-12 13:13:30.000000000 +0800
@@ -7,11 +7,14 @@
class Graphics
{
virtual void drawLine(int x0, int y0, int x1, int y1);
+ virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
+ virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
+ virtual void drawArc(double x, double y, double r);
virtual void drawArc(Point p, int r);
};
受 C++ 二進制兼容性方面的限制,我們不能這么做。其本質問題在於 C++ 以 vtable[offset] 方式實現虛函數調用,而 offset 又是根據虛函數聲明的位置隱式確定的,這造成了脆弱性。
我增加了 drawLine(double x0, double y0, double x1, double y1),造成 vtable 的排列發生了變化,現有的二進制可執行文件無法再用舊的 offset 調用到正確的函數。
怎么辦呢?有一種危險且丑陋的做法:把新的虛函數放到 interface 的末尾,例如:
--- old/graphics.h 2011-03-12 13:12:44.000000000 +0800
+++ new/graphics.h 2011-03-12 13:58:22.000000000 +0800
@@ -7,11 +7,15 @@
class Graphics
{
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
+
+ virtual void drawLine(double x0, double y0, double x1, double y1);
+ virtual void drawRectangle(double x0, double y0, double x1, double y1);
+ virtual void drawArc(double x, double y, double r);
};
這么做很丑陋,因為新的 drawLine(double x0, double y0, double x1, double y1) 函數沒有和原來的 drawLine() 函數呆在一起,造成閱讀上的不便。
這么做同時很危險,因為 Graphics 如果被繼承,那么新增虛函數會改變派生類中的 vtable offset 變化,同樣不是二進制兼容的。
另外有兩種似乎安全的做法,這也是 COM 采用的辦法:
1. 通過鏈式繼承來擴展現有 interface,例如從 Graphics 派生出 Graphics2
--- graphics.h 2011-03-12 13:12:44.000000000 +0800
+++ graphics2.h 2011-03-12 13:58:35.000000000 +0800
@@ -7,11 +7,19 @@
class Graphics
{
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
};
+
+class Graphics2 : public Graphics
+{
+ using Graphics::drawLine;
+ using Graphics::drawRectangle;
+ using Graphics::drawArc;
+
+ // added in version 2
+ virtual void drawLine(double x0, double y0, double x1, double y1);
+ virtual void drawRectangle(double x0, double y0, double x1, double y1);
+ virtual void drawArc(double x, double y, double r);
+};
將來如果繼續增加功能,那么還會有 class Graphics3 : public Graphics2;以及 class Graphics4 : public Graphics3 等等。
這么做和前面的做法一樣丑陋,因為新的 drawLine(double x0, double y0, double x1, double y1) 函數位於派生 Graphics2 interace 中,沒有和原來的 drawLine() 函數呆在一起,造成割裂。
2. 通過多重繼承來擴展現有 interface,例如定義一個與 Graphics class 有同樣成員的 Graphics2,再讓實現同時繼承這兩個 interfaces。
--- graphics.h 2011-03-12 13:12:44.000000000 +0800
+++ graphics2.h 2011-03-12 13:16:45.000000000 +0800
@@ -7,11 +7,32 @@
class Graphics
{
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
};
+
+class Graphics2
+{
+ virtual void drawLine(int x0, int y0, int x1, int y1);
+ virtual void drawLine(double x0, double y0, double x1, double y1);
+ virtual void drawLine(Point p0, Point p1);
+
+ virtual void drawRectangle(int x0, int y0, int x1, int y1);
+ virtual void drawRectangle(double x0, double y0, double x1, double y1);
+ virtual void drawRectangle(Point p0, Point p1);
+
+ virtual void drawArc(int x, int y, int r);
+ virtual void drawArc(double x, double y, double r);
+ virtual void drawArc(Point p, int r);
+};
+
+// 在實現中采用多重接口繼承
+class GraphicsImpl : public Graphics, // version 1
+ public Graphics2, // version 2
+{
+ // ...
+};
這種帶版本的 interface 的做法在 COM 使用者的眼中看起來是很正常的,解決了二進制兼容性的問題,客戶端源代碼也不受影響。
在我看來帶版本的 interface 實在是很丑陋,因為每次改動都引入了新的 interface class,會造成日后客戶端代碼難以管理。比如,如果代碼使用了 Graphics3 的功能,要不要把現有的 Graphics2 都替換掉?
如果不替換,一個程序同時依賴多個版本的 Graphics,一直背着歷史包袱。依賴的 Graphics 版本愈來愈多,將來如何管理得過來?
如果要替換,為什么不相干的代碼(現有的運行得好好的使用 Graphics2 的代碼)也會因為別處用到了 Graphics3 而被修改?
這種二難境地純粹是“以虛函數為庫的接口”造成的。如果我們能直接原地擴充 class Graphics,就不會有這些屁事,見本文“推薦做法”一節。
不要誤認為“接口一旦發布就不能更改”是天經地義的,那不過是“以 C++ 虛函數為接口”的固有弊端,如果跳出這個框框去思考,其實 C++ 庫的接口很容易做得更好。
為什么不能改?還不是因為用了C++ 虛函數作為接口。
Java 的 interface 可以添加新函數,C 語言的庫也可以添加新的全局函數,C++ class 也可以添加新 non-virtual 成員函數和 namespace 級別的 non-member 函數,這些都不需要繼承出新 interface 就能擴充原有接口。
偏偏 COM 的 interface 不能原地擴充,只能通過繼承來 workaround,產生一堆帶版本的 interfaces。有人說 COM 是二進制兼容性的正面例子,某深不以為然。COM 確實以一種最丑陋的方式做到了“二進制兼容”。脆弱與僵硬就是以 C++ 虛函數為接口的宿命。
相反,Linux 系統調用以編譯期常數方式固定下來,萬年不變,輕而易舉地解決了這個問題。在其他面向對象語言(Java/C#)中,我也沒有見過每改動一次就給 interface 遞增版本號的做法。
還是應了《The Zen of Python》中的那句話,Explicit is better than implicit, Flat is better than nested.
動態庫的接口的推薦做法
取決於動態庫的使用范圍,有兩類做法。
- 比如現在 Graphics 庫發布了 1.1.0 和 1.2.0 兩個版本,這兩個版本可以不必是二進制兼容。
用戶的代碼從 1.1.0 升級到 1.2.0 的時候要重新編譯一下,反正他們要用新功能都是要重新編譯代碼的。
如果要原地打補丁,那么 1.1.1 應該和 1.1.0 二進制兼容,而 1.2.1 應該和 1.2.0 兼容。
如果要加入新的功能,而新的功能與 1.2.0 不兼容,那么應該發布到 1.3.0 版本。
對於用戶可見的部分,升級時要注意二進制兼容性,選用合理的版本號;對於用戶不可見的部分,在升級庫的時候就不必在意。
- 如果庫的使用范圍很廣,用戶很多,各家的 release cycle 不盡相同,那么推薦 pimpl 技法[2, item 43], //還是用類方法的方式來對外暴露接口
並考慮多采用 non-member non-friend function in namespace [1, item 23] [2, item 44 abd 57] 作為接口。 //或者直接采用c語言的方式,全局函數來暴露接口
這里以前面的 Graphics 為例,說明 pimpl 的基本手法。
1. 暴露的接口里邊不要有虛函數,而且 sizeof(Graphics) == sizeof(Graphics::Impl*)。
class Graphics
{
public:
Graphics(); // outline ctor
~Graphics(); // outline dtor
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(Point p, int r);
private:
class Impl;
boost::scoped_ptr<Impl> impl;
};
2. 在庫的實現中把調用轉發 (forward) 給實現 Graphics::Impl ,這部分代碼位於 .so/.dll 中,隨庫的升級一起變化。
#include <graphics.h>
class Graphics::Impl
{
public:
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(Point p, int r);
};
Graphics::Graphics()
: impl(new Impl)
{
}
Graphics::~Graphics()
{
}
void Graphics::drawLine(int x0, int y0, int x1, int y1)
{
impl->drawLine(x0, y0, x1, y1);
}
void Graphics::drawLine(Point p0, Point p1)
{
impl->drawLine(p0, p1);
}
// ...
3. 如果要加入新的功能,不必通過繼承來擴展,可以原地修改,且保持二進制兼容性。先動頭文件:
--- old/graphics.h 2011-03-12 15:34:06.000000000 +0800
+++ new/graphics.h 2011-03-12 15:14:12.000000000 +0800
@@ -7,19 +7,22 @@
class Graphics
{
public:
Graphics(); // outline ctor
~Graphics(); // outline dtor
void drawLine(int x0, int y0, int x1, int y1);
+ void drawLine(double x0, double y0, double x1, double y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
+ void drawRectangle(double x0, double y0, double x1, double y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
+ void drawArc(double x, double y, double r);
void drawArc(Point p, int r);
private:
class Impl;
boost::scoped_ptr<Impl> impl;
};
然后在實現文件里增加 forward,這么做不會破壞二進制兼容性,因為增加 non-virtual 函數不影響現有的可執行文件。
--- old/graphics.cc 2011-03-12 15:15:20.000000000 +0800
+++ new/graphics.cc 2011-03-12 15:15:26.000000000 +0800
@@ -1,35 +1,43 @@
#include <graphics.h>
class Graphics::Impl
{
public:
void drawLine(int x0, int y0, int x1, int y1);
+ void drawLine(double x0, double y0, double x1, double y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
+ void drawRectangle(double x0, double y0, double x1, double y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
+ void drawArc(double x, double y, double r);
void drawArc(Point p, int r);
};
Graphics::Graphics()
: impl(new Impl)
{
}
Graphics::~Graphics()
{
}
void Graphics::drawLine(int x0, int y0, int x1, int y1)
{
impl->drawLine(x0, y0, x1, y1);
}
+void Graphics::drawLine(double x0, double y0, double x1, double y1)
+{
+ impl->drawLine(x0, y0, x1, y1);
+}
+
void Graphics::drawLine(Point p0, Point p1)
{
impl->drawLine(p0, p1);
}
采用 pimpl 多了一道 forward 的手續,帶來的好處是可擴展性與二進制兼容性,通常是划算的。pimpl 扮演了編譯器防火牆的作用。
pimpl 不僅 C++ 語言可以用,C 語言的庫同樣可以用,一樣帶來二進制兼容性的好處,比如 libevent2 里邊的 struct event_base 是個 opaque pointer,客戶端看不到其成員,都是通過 libevent 的函數和它打交道,這樣庫的版本升級比較容易做到二進制兼容。
為什么 non-virtual 函數比 virtual 函數更健壯?因為 virtual function 是 bind-by-vtable-offset,而 non-virtual function 是 bind-by-name。加載器 (loader) 會在程序啟動時做決議(resolution),通過 mangled name 把可執行文件和動態庫鏈接到一起。就像使用 Internet 域名比使用 IP 地址更能適應變化一樣。
萬一要跨語言怎么辦?很簡單,暴露 C 語言的接口。Java 有 JNI 可以調用 C 語言的代碼,Python/Perl/Ruby 等等的解釋器都是 C 語言編寫的,使用 C 函數也不在話下。C 函數是 Linux 下的萬能接口,C 語言是最偉大的系統編程語言。
本文只談了使用 class 為接口,其實用 free function 有時候更好(比如 muduo/base/Timestamp.h 除了定義 class Timestamp,還定義了 muduo::timeDifference() 等 free function),這也是 C++ 比 Java 等純面向對象語言優越的地方。留給將來再細談吧。
參考文獻
[1] Scott Meyers, 《Effective C++》 第 3 版,條款 35:考慮 virtual 函數以外的其他選擇;條款 23:寧以 non-member、non-friend 替換 member 函數。
[2] Herb Sutter and Andrei Alexandrescu, 《C++ 編程規范》,條款 39:考慮將 virtual 函數做成 non-public,將 public 函數做成 non-virtual;條款 43:明智地使用 pimpl;條款 44:盡可能編寫 nonmember, nonfriend 函數;條款 57:將 class 和其非成員函數接口放入同一個 namespace。
[3] 孟岩,《function/bind的救贖(上)》,《回復幾個問題》中的“四個半抽象”。
[4] 陳碩,《以 boost::function 和 boost:bind 取代虛函數》,《朴實的 C++ 設計》。
================
https://blog.csdn.net/enlyhua/article/details/84561222


-
5.環境變量
LD_LIBRARY_PATH :
6.1 共享庫的創建
