web-dev-qa-db-ja.com

静的にリンクされたライブラリ間のシンボルの衝突に対処する方法は?

ライブラリを記述する際の最も重要なルールとベストプラクティスの1つは、ライブラリのすべてのシンボルをライブラリ固有の名前空間に入れることです。 namespaceキーワードにより、C++はこれを簡単にします。 Cでは、通常の方法は、識別子にライブラリ固有のプレフィックスを付けることです。

C標準の規則は、それらにいくつかの制約を課します(安全なコンパイルのために):Cコンパイラは、識別子の最初の8文字のみを見ることがあるため、foobar2k_eggsおよびfoobar2k_spamは同じ識別子として有効に解釈される場合がありますが、現代のコンパイラはすべて任意の長い識別子を許可しているため、私たちの時代(21世紀)にこれを気にする必要はありません。

しかし、シンボル名/識別子を変更できないライブラリに直面している場合はどうでしょうか?たぶん、あなたは静的バイナリとヘッダーだけを持っているか、したくないか、自分で調整して再コンパイルすることを許可されていません。

77
datenwolf

少なくともstaticライブラリの場合、非常に便利に回避できます。

ライブラリのヘッダーfooおよびbarを検討してください。このチュートリアルのために、ソースファイルも提供します。

examples/ex01/foo.h

int spam(void);
double eggs(void);

examples/ex01/foo.c(これは不透明であるか、利用できない場合があります)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

example/ex01/bar.h

int spam(int new_spams);
double eggs(double new_eggs);

examples/ex01/bar.c(これは不透明であるか、利用できない場合があります)

int the_spams;
double the_eggs;

int spam(int new_spams)
{
    int old_spams = the_spams;
    the_spams = new_spams;
    return old_spams;
}

double eggs(double new_eggs)
{
    double old_eggs = the_eggs;
    the_eggs = new_eggs;
    return old_eggs;
}

プログラムfoobarでそれらを使用したい

example/ex01/foobar.c

#include <stdio.h>

#include "foo.h"
#include "bar.h"

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

1つの問題がすぐに明らかになります。Cはオーバーロードを知りません。そのため、同じ名前で署名が異なる2倍の関数があります。したがって、それらを区別する方法が必要です。とにかく、これについてコンパイラーが言わなければならないことを見てみましょう:

example/ex01/ $ make
cc    -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1

さて、これは驚きではありませんでした。それは、私たちがすでに知っていること、または少なくとも疑っていることを伝えただけです。

元のライブラリのソースコードやヘッダーを変更せずに、識別子の衝突をなんとか解決できますか?実際にできます。

まず、コンパイル時の問題を解決しましょう。このために、ヘッダーインクルードをプリプロセッサの束で囲みます#defineディレクティブは、ライブラリによってエクスポートされたすべてのシンボルの前に付けられます。後でニースの居心地の良いラッパーヘッダーでこれを行いますが、何が起こっているかを示すために、foobar.cソースでそれを逐語的に行いましたファイル:

example/ex02/foobar.c

#include <stdio.h>

#define spam foo_spam
#define eggs foo_eggs
#  include "foo.h"
#undef spam
#undef eggs

#define spam bar_spam
#define eggs bar_eggs
#  include "bar.h"
#undef spam
#undef eggs

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
           bar_spam(new_bar_spam), new_bar_spam, 
           bar_eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

これをコンパイルすると...

example/ex02/ $ make
cc    -c -o foobar.o foobar.c
cc   foobar.o foo.o bar.o   -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1

...最初は状況が悪化したように見えます。しかし、よく見てください。実際、コンパイル段階はうまくいきました。リンカだけが、シンボルが衝突していると不平を言っており、これが発生する場所(ソースファイルと行)を教えてくれます。そして、私たちが見ることができるように、それらのシンボルは接頭辞なしです。

nmユーティリティを使用して、シンボルテーブルを見てみましょう。

example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

そのため、これらのシンボルを不透明なバイナリにプレフィックスするという課題に挑戦しています。はい、この例の過程でソースがあり、そこで変更できることを知っています。ただし、現時点では、これらの。oファイル、またはa。a(実際には。oの束です)。

objcopy救助へ

特に興味深いツールが1つあります:objcopy

objcopyは一時ファイルに対して機能するため、インプレースで動作しているかのように使用できます。 -prefix-symbolsと呼ばれるオプション/操作が1つあり、3つの推測があります。

頑固なライブラリにこのファラを投げましょう:

example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o

nmは、これが機能しているように見えることを示しています。

example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams

example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams

この全体をリンクしてみましょう:

example/ex03/ $ make
cc   foobar.o foo.o bar.o   -o foobar

そして実際、それは機能しました:

example/ex03/ $ ./foobar 
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000

ここで、nmを使用してライブラリのシンボルを自動的に抽出し、ラッパーヘッダーファイルを書き込むツール/スクリプトを実装するための演習として、読者に任せます。構造の

/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */

objcopyを使用して、静的ライブラリのオブジェクトファイルにシンボルプレフィックスを適用します。

共有ライブラリはどうですか?

原則として、共有ライブラリでも同じことが可能です。しかし、共有ライブラリは、その名前が示すように、複数のプログラム間で共有されるため、この方法で共有ライブラリを操作することはあまり良い考えではありません。

トランポリンラッパーを作成することはできません。さらに悪いことに、オブジェクトファイルレベルで共有ライブラリにリンクすることはできませんが、動的な読み込みを強制されます。しかし、これは独自の記事に値します。

お楽しみに、そしてコーディングをお楽しみください。

125
datenwolf

C標準の規則はそれらにいくつかの制約を課します(安全なコンパイルのために):ACコンパイラは識別子の最初の8文字のみを見ることがあります。長い識別子であるため、私たちの時代(21世紀)にこれを気にする必要はありません。

これは、最新のコンパイラの単なる拡張ではありません。また、現在のC標準必須かなり長い外部名をサポートするコンパイラー。正確な長さは忘れましたが、今覚えていれば31文字のようになっています。

しかし、シンボル名/識別子を変更できないライブラリに直面している場合はどうでしょうか?たぶん、あなたは静的バイナリとヘッダーだけを持っているか、したくないか、自分で調整して再コンパイルすることを許可されていません。

その後、あなたは立ち往生しています。ライブラリの作者に文句を言ってください。 DebianのlibSDLリンクlibsoundfileが原因で、アプリケーションのユーザーがDebianでビルドできないというバグに一度遭遇しました。(少なくともその時点では)グローバル名前空間をひどく変数で汚染していましたdsp(子供じゃない!)私はDebianに苦情を申し立て、彼らはパッケージを修正し、修正をアップストリームに送りました。

問題を解決するため、これが最良のアプローチだと本当に思います全員。あなたが行うローカルハックは、ライブラリに問題を残し、次の不幸なユーザーが再び遭遇して戦うことになります。

本当に簡単な修正が必要で、ソースがある場合は、-Dfoo=crappylib_foo -Dbar=crappylib_barなどをメイクファイルに修正します。そうでない場合は、見つけたobjcopyソリューションを使用します。

7
R..

GCCを使用している場合、-allow-multiple-definitionリンカースイッチは便利なデバッグツールです。これにより、リンカは最初の定義を使用するようになります(それについては言いません)。それについての詳細 こちら

これは、開発者がベンダー提供のライブラリのソースを利用でき、何らかの理由でライブラリ関数にトレースする必要があるときに役立ちました。このスイッチを使用すると、ソースファイルのローカルコピーをコンパイルおよびリンクし、変更されていない静的ベンダーライブラリにリンクすることができます。発見の航海が完了したら、makeシンボルからスイッチをヤンクすることを忘れないでください。意図的なネームスペースの衝突を伴うリリースコードの出荷は、意図しないネームスペースの衝突を含む落とし穴になりやすいです。

3
JJJSchmidt