邪惡的菱形相依性

1235357885|%Y-%m-%d|agohover

會提出這個議題,是因為敝實驗室之前發展的一套影像函式庫 libgil2 (這是一個大量使用 template 的影像函式庫,功能包括讀取、寫入圖檔及簡單的影像處理功能)。因為 libgil2 在檔案處理的部份用了許多其它函式庫,包含 libpng, jpeg, OpenEXR 等等,因此我們有考慮將這些相依的模組一起連結成一套函式庫,然而這也造成了一些潛在的問題…

先考慮以下的簡單狀況:

common.h:

#ifndef COMMON_H
#define COMMON_H
#ifdef _MSC_VER
    #define EXPORT __declspec(dllexport)
    #define IMPORT __declspec(dllimport)
#else
    #define EXPORT
    #define IMPORT
#endif
#endif // COMMON_H

foo.c:

#include <stdio.h>
void foo(void)
{
    static int i = 0;
    printf("%d\n", i++);
}

bar.c:

#include "common.h"
void foo(void);
EXPORT void baz(void)
{
    foo();
}

baz.c:

#include "common.h"
void foo(void);
EXPORT void baz(void)
{
    foo();
}

main.c:

#include "common.h"
IMPORT void bar(void);
IMPORT void baz(void);
 
int main(void)
{
    bar();
    bar();
 
    baz();
    baz();
 
    return 0;
}

依照一般的認知來說,這段程式碼應該會產生如下的輸出:

0
1
2
3
diamond-dependency.png

然而這段程式碼展示了一種菱形的相依性關係:main 使用 bar 和 baz 這兩個模組,而 bar 和 baz 同樣使用了 foo。如果我們把 foo 編譯為靜態函式庫,而 bar 和 baz 都個別編譯為動態函式庫並靜態連結 foo,問題就開始浮現了:

diamond-dependency-dll.png

如圖所示,兩個動態函式庫都各自封裝了一份 foo,因此 foo 內部的程式碼及變數就會平白多出一份。上述的程式碼若以這樣的方式連結,執行結果將會是:

0
1
0
1

有趣的是,這是 Windows 上的結果。若在 Linux 下,動態連結時似乎會偵測到這種情況,因此執行時 foo 的實體還是只有一份!結果依然和最前面的一般認知相同。

那麼,如果 bar 和 baz 呼叫的是同一份 foo,要是這兩套函式庫使用了不同版本的 foo 呢?哪個版本才會被呼叫到?根據我的實驗結果,答案是看程式進行 link 時的參數順序:

gcc -o main main.cpp -lbar -lbaz # 使用 bar 內的 foo
gcc -o main main.cpp -lbaz -lbar # 使用 baz 內的 foo

不管是像 Windows 那樣,在每個 DLL 中個自封裝一份 foo,或是 Linux 上自動把重覆的去掉,其實都是有問題的。不管函式庫的連結方式以及連結順序,程式碼的行為都應該一致。要避免這個問題其實也很簡單:別混用動態與靜態函式庫。要嘛就全部編成靜態,不然就全部編成動態。

回到 libgil2,如果我們編出一套靜態連結 libpng, jpeg, OpenEXR 的懶人包,那這個懶人包大概只能用在寫命令列模式的小程式。許多 GUI 函式庫都會使用到 libpng 來載入圖檔,因此要是使用這套懶人包開發 GUI 程式,勢必發生上述的菱形相依性問題。


Comments

Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License