Apple Developer Connection
高度な検索
Member Login ログイン | ご入会 ADC連絡先

Mac OS XのためのC++のヒントとこつ

C++プログラマのための注目ポイント





はじめに

このドキュメントの目的は、最新の複雑なC++ Mach-OアプリケーションをMac OS Xに導入する作業を容易にすることです。よく生じるいくつかの問題に対して、役に立ついくつかのヒントを示します。網羅的なリファレンスといえるものではありませんが、悩めるC++デベロッパを正しい方向に導き、必要に応じてさらに詳細な情報を入手する手だてを提供します。

先頭に戻る

可視性オプションの選択

GCCでは、可視性とは、ほかのツールでいうダイナミックライブラリのインポート/エクスポートのことです。ただし、GCCのシンボルは可視か隠蔽されているかのどちらかしかありません。可視シンボルは、共有ライブラリのエントリポイントとなります。詳細については、「Controlling Symbol Visibility」を参照してください。ここでは、概要を述べるに留めます。

Cでは、どのシンボルを可視にし、どれを隠蔽するかを決めるのは比較的簡単です。共有ライブラリのエントリポイントは、可視であることが必須です。そのほかはすべて隠蔽できます。C++では、多くの定義はソースファイルではなくヘッダファイルに含まれます。C++のヘッダファイルには、インターフェイスとプライベート実装の詳細を含めることができます。たとえば、テンプレート(クラスと関数の両方)、クラス宣言、インライン関数(メンバと非メンバの両方)などです。

可視性を宣言する方法は4つあります。

  1. コマンドラインオプション(-fvisibility=hiddenなど)

  2. プラグマ

  3. 個々の型と関数の属性宣言

  4. Export/unexportリスト

上記のそれぞれの方法の役割と、それらが相互にどのように作用するのかを理解することが大切です。はじめの3つの方法は、コンパイル時に可視性を宣言します。4番目はリンク時の操作です。エクスポートリストは、コンパイラが可視とマークしたシンボルを隠蔽することができますが、コンパイラが隠蔽するものとしてとマークしたシンボルを、エクスポートリストを使って可視にすることはできません。

可視性のコマンドラインオプション

可視性コマンドラインオプションは、可視性をデフォルト設定する際に役立ちます。特に、サードパーティのソースコードや、何らかの理由で属性またはプラグマで修飾することが実用的ではないソースコードの場合に役に立ちます。コマンドライン可視性オプションを指定しない場合、シンボルは可視とマークされます(ただし、ほかの3つの方法のいずれかでオーバーライドされないかぎり)。デフォルトの可視性を明示的に選択するには、次のオプションをコマンドラインに指定します。

    -fvisibility=default

コマンドラインに-fvisibility=hiddenを指定すると、すべてのシンボルが隠蔽されるものとしてマークされます(ただし、プラグマまたは属性でオーバーライドされない場合です。エクスポートリストでは、隠蔽されたシンボルを可視にすることができません)。

便利と考えられる可視性に関連するコマンドオプションは、このほかに2つあります。

  1. -fvisibility-inlines-hiddenは、2つのメンバ関数のアドレスが異なるリンク対象ユニットで取得されている場合、ポインタをインラインメンバ関数と照合しないことを宣言するときに使用します。

    多くのアプリケーションでは、-fvisibility-inlines-hiddenは、多数のシンボルを隠蔽する安全かつ簡単な方法であり、読み込み時間の短縮につながります。ダイナミックリンカは読み込み時に、隠蔽シンボルに対するよりも多くの処理を可視シンボルに対して行います。

    このスイッチの動作は、メンバ関数を隠蔽するものとして直接マークするのと同じとはいえません。なぜなら、このスイッチは関数の静的なローカル変数に影響を与えず、関数が単独のリンク対象ユニットで定義されているとコンパイラに推測させないからです。静的ローカル変数はすべて、コマンドラインスイッチと関係なく、そのとき有効な可視性の設定が適用されます。

    注:

    • コマンドラインに、-fvisibility=hiddenも同時に指定されている場合、-fvisibility-inlines-hiddenは何の効果もありません。

    • -fvisibility-inlines-hiddenは、非メンバインライン関数に影響を与えません。

    • 共有ライブラリの境界を越えてポインタをインラインメンバ関数と照合する場合、-fvisibility-inlines-hiddenを使用しないでください。

  2. fvisibility-ms-compatは、型と関連付けられたtype_infoデータを除いてデフォルトの可視性を隠蔽の設定にします。これにより、Microsoft Visual Studioのリンクモデルがエミュレートされます。例外の捕捉、dynamic_casttypeid表現を使用して取得したtype_infoの比較には、型の比較が重要です。

    注:

    • コマンドラインに、-fvisibility-ms-compatも同時に指定されている場合、-fvisibility=hiddenは何の効果もありません。

    • コマンドラインに、-fvisibility-ms-compatも同時に指定されている場合、-fvisibility-inlines-hiddenは何の効果もありません。

    • -fvisibility-ms-compatを使用した場合、異なる共有ライブラリ内の個別に開発された2つの実装詳細クラスが、偶発的に同じ型と間違われる可能性があります。

    • -fvisibility-ms-compatフラグは、型のすべての静的データメンバを隠蔽し、各リンク対象ニットにプライベートのコピーを供給します(ただし、対象データが可視とマークされている場合を除く)。

先頭に戻る

プラグマを使用した可視性の設定

プラグマを使って、個々のシンボルまたはシンボルのグループの可視性を制御できます。そのようにプラグマを使うことで、可視性コマンドラインオプションとは関係なく、影響を受けるシンボルの可視性を制御できます。その結果、保守の観点から、ソースコードの堅牢性が増す可能性があります。

コンパイラのデフォルトモードを可視の設定にするには、次のように指定します。

#pragma GCC visibility push(default)

コンパイラのデフォルトモードを隠蔽の設定にするには、次のように指定します。

 #pragma GCC visibility push(hidden)

直前に行ったGCC visibility pushプラグマを解除するには、次のように指定します。

 #pragma GCC visibility pop

これによって、可視性プラグマの対象範囲が形成されます。対象範囲はネストすることが可能であり、最も内側の対象範囲が優先されます。

注:よくある誤りの1つは、プラグマの綴り間違いです。この場合、-Wunknown-pragmasを指定して(これは、不明(綴り間違い)のプラグマがあると警告を発します)コンパイルしないかぎり、警告なしに何も行われません。

コンパイラの型に関するメタデータ(type_infoなど)の可視性を制御するには、型宣言を囲みます。

    #pragma GCC visibility push(default)

    class MyType
    {
        // ...
    };

    #pragma GCC visibility pop

先頭に戻る

属性を使用した可視性の設定

属性の個々のシンボルの可視性を制御できます。これによって、宣言自身と宣言の可視性が結び付けられ、コマンドラインオプションおよびプラグマのどちらからも独立なものとなります。この手法を使うと、コードを移動することで起こりがちな可視性の制御に関するエラーが減ります。

たとえば、関数を可視とマークするには、次のように指定します。

  __attribute__((__visibility__("default"))) void MyFunction1() {}

そして関数を隠蔽とマークするには、次のように指定します。

__attribute__((__visibility__("hidden"))) void MyFunction2() {}

こうした可視性の詳細を包含するアプリケーション用のマクロを作成すると便利です。このようなマクロを作成しておけば、可視性の概念がない環境や可視性の指定に異なる構文を使用する環境にコードを移植するのが簡単になります。たとえば、次のようにします。

    #define PUBLIC  __attribute__((__visibility__("hidden")))
    #define PRIVATE __attribute__((__visibility__("default")))

    PUBLIC  void MyFunction1() {}
    PRIVATE void MyFunction2() {}

次のようにclassstructをマークできます。

    class PUBLIC MyClass {/*...*/};

属性は、type_infoデータだけでなくメンバ関数、静的データメンバもマークします。クラスの個々のメンバ関数(プライベートメンバなど)にも属性を指定できます。

先頭に戻る

エクスポートリストを使用したシンボルの隠蔽

このテクニカルノートで紹介する最後のツールは、エクスポートリストです。これは単純に、どのシンボルを隠蔽するべきかをリンカに指示する(マングル形式でなければならない)シンボルのリストです。形式は2種類あります。

  1. -exported_symbols_list $FILENAMEと指定して、どのシンボルを可視に設定するのかをリンカに指示し、残りをすべて隠蔽します。

    エクスポートシンボルリストに指定されているシンボルは、コンパイラによって可視としてマークされている必要があり、そうでないシンボルは隠蔽されます。

  2. -unexported_symbols_list $FILENAMEと指定して、どのシンボルを隠蔽に設定するのかをリンカに指示します。

    コンパイラによって可視としてマークされ、非エクスポートシンボルリストに指定されていないシンボルは可視になります。

先頭に戻る

シンボルの可視性のまとめ

上記の可視性ツールを組み合わせて使うと便利な場合があります。たとえば、隠蔽すべき多数のシンボルと、可視にすべき少数のシンボルがライブラリにある場合、-fvisibility=hiddenを使用してシンボルを隠蔽し、可視にする必要のある少数のシンボルについてのみソースコードを修飾します。

共有ライブラリの境界を越えてthrowまたはdynamic_castを行う型の場合、上記のいずれかの方法を使用して可視としてマークする必要があります。可視としてマークしなければ、catch節がスローされた例外を捕捉できないというようなランタイムエラーが発生します。たとえば、次のようにします。

    // MyException.h

    class MyException {};

    // 共有ライブラリ:

    #include "MyException.h"

    void my_func()
    {
        throw MyException();
    }

    // アプリケーション:

    #include "MyException.h"

    int main()
    {
        try
        {
            my_func();
        }
        catch (MyException&)
        {
            // catchは、MyExceptionが隠蔽されると失敗する
            // 隠蔽されている場合、スローされるMyExceptionは、catch節の中で
            //    参照されるMyExceptionとは異なる型になる
        }
    }

先頭に戻る

Xcodeテンプレートの選択

Xcodeで「新規プロジェクト」を選ぶと、選択可能なさまざまな種類のXcodeテンプレートが表示されます。C++デベロッパは、どのテンプレートで始めるべきでしょうか?どのテンプレートからでも開始して、すべてを自分で設定することが可能です。しかし、最初に最適なテンプレートを選べば、デフォルト設定で望みどおりのことが行われる可能性が高くなります。

4つのアプリケーションタイプに分散して、C++デベロッパ向けに作成された6つのXcodeテンプレートが用意されています。

  • Application(アプリケーション)

    • Carbon C++ Application(Carbon C++アプリケーション)

    • Carbon C++ Standard Application(Carbon C++標準アプリケーション)

  • Bundle(バンドル)

    • Generic C++ Plugin(汎用C++プラグイン)

  • Command Line Utility(コマンドラインユーティリティ)

    • C++ Tool(C++ツール)

  • Dynamic Library(ダイナミックライブラリ)

    • C++ Dynamic Library(C++ダイナミックライブラリ)

    • C++ Standard Dynamic Library(C++標準ダイナミックライブラリ)

アプリケーションテンプレート

Carbonアプリケーションテンプレートのいずれか1つを選び、Carbon APIに基づく、たたき台となるC++アプリケーションを作成します。たたき台のアプリケーションは、通常のウインドウコマンド(新規、閉じる、最小化など)に応答するOS Xアプリケーションウインドウを表示します。このテンプレートには、「About」ウインドウの例もあります。テンプレートは、C++のスタイルで、イベントをキャッチしてカスタマイズする方法、およびnibファイルから読み取りを行う方法も示します。

「Carbon C++ Application」と「Carbon C++ Standard Application」の唯一の違いは、デフォルトの可視性の設定です。「Standard」(標準)のテンプレートは、デフォルトで可視に設定されていますが、もう一方のテンプレートは隠蔽に設定されます。デフォルトで隠蔽の可視性を選んだ場合、共有ライブラリにリンクするとき、共有ライブラリの境界を越えて知られている必要のあるシンボルを、属性修飾またはプラグマを使って明示的に可視として宣言する必要があります。これには、try/catchdynamic_castの実装で使用されるtype_infoも含まれます。可視のシンボルを最小限にすると、リンク対象ユニット間でABIの安定性が増し、読み込み時間も短縮します。「Standard」(標準)テンプレートを使用すると、これらのC++言語機能がすべて想定どおりに機能し、デベロッパは手を加える必要がありません。

先頭に戻る

汎用C++プラグイン

このプロジェクトは、Cインターフェイスを公開し、静的C++標準ライブラリを使用し、そしてデバッグにDwarfを使用する汎用のC++テンプレートをビルドします。

このテンプレートは、サンプルの「プラグイン」またはバンドルを作成します。これは、アプリケーションの存続期間中、動的にロードおよびアンロードできる共有ライブラリです。サンプルのプラグインは設計上、Cインターフェイスのみをエクスポートし、例外をスローしません。しかし内部はC++で実装されています。デフォルトで可視性は隠蔽の設定になっており、静的にlibstdc++をリンクします。これにより内部で例外のスローとキャッチが可能になりますが、クライアントは幸せなことに、この事実を知ることはありません。

テンプレートは、プラグマを使ってそのCインターフェイスを可視としてマークする方法を示します。さらに、内部的にのみ使われることを目的とした(そして可視性が隠蔽するものとしてマークされている)C++宣言を含んだ「プライベートヘッダ」も示します。

先頭に戻る

C++コマンドラインツール

単純なHelloWorldやグラフィカルユーザインターフェイスファイルを持たない(標準C++コンソールおよびI/Oに限定した)プログラムを作成する場合は、このテンプレートから始めます。「Hello, World!」とだけ出力する単独のmain.cppから始まります。Xcodeでビルドして実行すると、コンソール出力はXcodeの実行ログに表示されます。このアプリケーションは、「ターミナル」アプリケーションでも実行できます。

C++コマンドラインツールは、デフォルトで可視性が隠蔽の設定になっています。デフォルトで可視にしたい場合、「プロジェクト」メニューの下にある「アクティブターゲットを編集」を選択し、表示されるダイアログの中の検索ボックスに「vis」と入力し、「デフォルトで隠されたシンボル」と「インライン関数を隠す」のチェックを外します。

先頭に戻る

ダイナミックライブラリ

これらのテンプレートは、サンプルとなるダイナミック(共有)ライブラリを準備します。サンプルには、パブリック(可視)インターフェイスおよびプライベート(隠蔽)内部ヘッダの両方が含まれます。アプリケーションテンプレートと同様に、「Standard」(標準)テンプレートはデフォルトですべて可視ですが、名前に「Standard」が含まれていないテンプレートはデフォルトで可視性が隠蔽に設定されます。

先頭に戻る

new/deleteのオーバーライド

C++標準規格(ISO番号14882-2003)によると、次の8つのシグネチャをクライアントコードで置き換えることができます。

    void* operator new(std::size_t size) throw(std::bad_alloc);
    void* operator new(std::size_t size, const std::nothrow_t&) throw();
    void  operator delete(void* ptr) throw();
    void  operator delete(void* ptr, const std::nothrow_t&) throw();

    void* operator new[](std::size_t size) throw(std::bad_alloc);
    void* operator new[](std::size_t size, const std::nothrow_t&) throw();
    void  operator delete[](void* ptr) throw();
    void  operator delete[](void* ptr, const std::nothrow_t&) throw();

完全なコントロールと移植性のためには、1つでもシグネチャを置き換えるのであれば、すべてを置き換えるべきです。ただし、これらの配列形式のデフォルトの実装は単純に非配列形式へと転送を行うだけです。4つの非配列形式のみを置き換える場合は、デフォルトの配列形式から、置き換えた非配列形式に転送があるものと想定してください。

置き換えた項目は、アプリーション全体に渡って有効です。ほかのリンク対象ユニット(共有ライブラリ)のコードも、置き換えられたnewdeleteを呼び出します。アプリケーション全体(およびすべてのリンク対象ユニット)に渡って、置き換えられたnewdeleteの定義は1つだけである必要があります。そうしておくことで、メモリの所有権が共有ライブラリの境界を越えて移転された場合でも、確実に正しく削除されます。

一般的に、それが共有ライブラリの唯一の目的でなければ、共有ライブラリにおいてこれらの演算子をオーバーライドするべきではありません。そうでなければ、アプリケーションはオーバーライドされたnew/deleteを複数の定義にリンクする可能性が高くなります。

ごくまれな状況で、共有ライブラリにこれらの演算子のプライベートな定義があると便利な場合があります。これは、-unexported_symbols_list filenameフラグを指定してリンクし、非エクスポートファイルに次のシンボルを指定することで実現します。

    __Znwm
    __Znwm.eh
    __ZnwmRKSt9nothrow_t
    __ZnwmRKSt9nothrow_t.eh
    __ZdlPv
    __ZdlPv.eh
    __ZdlPvRKSt9nothrow_t
    __ZdlPvRKSt9nothrow_t.eh

    __Znam
    __Znam.eh
    __ZnamRKSt9nothrow_t
    __ZnamRKSt9nothrow_t.eh
    __ZdaPv
    __ZdaPv.eh
    __ZdaPvRKSt9nothrow_t
    __ZdaPvRKSt9nothrow_t.eh

そうすることで、メモリ所有権がこの共有ライブラリに移転したり、ほかへ移転したりしないことを作者が保証する必要があります。メモリ所有権の移転は、参照をカウントするオブジェクト(std::stringなど)を渡したり、ヒープに割り当てられたメッセージ(std::runtime_errorなど)を含んだ例外をスローしたり、またはリソースを割り当てるコンストラクタをインライン指定し、対応するデストラクタをインライン指定しなかったり(あるいはその逆をしたり)するなど、ちょっとしたことで起きます。

現在のツールセットにはバグがあります。それによって、変換ユニットがこれらの演算子を置き換え、弱いリンク(たとえば、インライン関数や暗黙のテンプレートのインスタンス化)を持つシンボルを含まない場合、リンカは置き換えられた演算子new/deleteを見つけられません。現実のほとんどの変換ユニットは、弱いリンクのシンボルを含むので、通常は問題になりません。しかし、このバグの影響を受ける場合は、次の行を演算子の定義の直後に追加します。

    __attribute__((__weak__, __visibility__("default"))) int dummy_weak_symbol_for_new;

先頭に戻る

名前空間のusing宣言のためのstrong属性

次のコードを考えてみましょう。

    namespace Acme
    {

    template <class T>
    class V
    {
    };

    template <class T>
    void func1(T&) {}

    template <class T>
    void func2(T&) {}

    } // Acme

    // Acmeクライアント

    struct MyType {};

    namespace Acme
    {

    template <>
    class V<MyType>
    {
    };

    }  // Acme

    int main()
    {
        Acme::V<int> v_int;
        func1(v_int);
        Acme::func2(v_int);
        Acme::V<MyType> v_mytype;
        func1(v_mytype);
        Acme::func2(v_mytype);
    }

Acmeという名前のライブラリとそれを使うクライアントがあったとします。クライアントは、(引数に応じた参照に基づき)Acme名前空間内の関数を呼び出し、また、クライアント定義型のAcmeテンプレートを特殊化します。ここまですべて順調です。

今度は、Acmeが何らかの理由で、その機能の一部をAcmeのネストした名前空間内に配置し、それをusing宣言を使ってAcme名前空間にインポートするとします。目的は、APIを安定に保ちつつ、秩序立った方法でライブラリのABIを変更することです(つまり、新しいライブラリを対象に再コンパイルするコードを変更する必要はないけれども、新しくマングリングされます)。このようなことをする理由は後述します。

// Acmeライブラリ

namespace Acme
{

namespace _1
{

template <class T>
class V
{
};

template <class T>
void func1(T&) {}

}  // _1

using namespace _1;

template <class T>
void func2(T&) {}


} // Acme

これは、ほとんどのクライアントコードで機能します。理由は、クライアントコードはネストした名前空間を知らなくてもよく、Acme::VおよびAcme::func1を使い続けてよいからです。ただし、前述のクライアントコードは動作しなくなります。

error: specialization of 'template<class T> class Acme::_1::V' in different namespace

すなわち、クライアントはAcme::V<T>を特殊化することができません。代わりに、Acme::_1::V<T>を特殊化する必要がありますが、ネストされている名前空間をAPIレベルで意識させたくなかったのでこれは残念です。しかし、この修正を加えてもすべて直るわけではありません。

error: 'func2' was not declared in this scope

このエラーは、クライアントのコード内にある両方のfunc2を指します。

func2(v_int);
func2(v_mytype);

この時点でVAcme::_1にいるので、func2に関して名前空間Acmeを探すことはありません。

GCCコンパイラには、名前空間がネストしていることをクライアントが認識する必要なくこのコードを機能させる拡張があります。

 using namespace _1 __attribute__((__strong__));

これによって、クライアントの元のコードは、Acme::V<T>の特殊化も含め、問題なく機能します。しかもVfunc1は、ネストした名前空間Acme::_1に基づいてマングリングされます。

APIに混乱を招く恐れがあるのに、Acmeが意図的にこれを導入するのはなぜでしょう?

先頭に戻る

名前空間によるバージョン管理

「名前空間のusing宣言のためのstrong属性」のセクションをまだ読んでいなければ、まずは読むことをお勧めします。このセクションは、前のセクションの最後に出てきた疑問に答えます。なぜなのでしょう?

このセクションのタイトルから推測できるかもしれませんが、前のセクションで説明した方法を使って、ライブラリのAPIに変更を加えることなく(あるいは、少なくとも上位互換性を保ったAPI変更にとどめ)、ライブラリのABIのバージョニングができます。共有ライブラリの文脈において、これは共有ライブラリの複数バージョンを同時に出荷できること、および、互換性のないバージョンが混在することによる暗黙のランタイムエラーの発生を危惧することなく、単独のアプリケーションを複数のバージョンと間接的にリンクできることを意味しています。

次の状況を考えてください。

Acmeに、Acme.1.dylibおよびAcme.2.dylibという2つのバージョンの出荷用の共有ライブラリが含まれていたとします。ほかの共有ライブラリ(Lib_AとLib_B)も、価値あるサービスを利用するためにAcmeにリンクしています。Lib_Bは、Acme.2.dylibを対象に再コンパイルされていますが、Lib_Aは依然としてAcme.1.dylibを使用しています。最後に、あるアプリケーションがLib_AとLib_Bの両方とリンクし、事実上Acmeの2つのバージョンを同一のプロセスに混在させています。

ここでたとえば、ABIを維持しない方法でAcmeのAcme::func1に変更が加えられた場合(たとえば、シグネチャは変えずにセマンティックスを少しだけ変えるなどして)、Acme::func1の2つの互換性のないバージョンを同じプロセスで機能させると、大きな問題が生じる可能性があります。アプリケーション本体がAcme::func1を呼び出したなら、どうなるでしょう。どちらのバージョンが対象になるでしょう。

このシナリオは、名前空間によるバージョニングによって対応できます。名前空間は、シンボルの名前にマングリングされるので、名前空間Acme内の隠されている名前空間は、func1にマングリングされます。オブジェクトコードレベルでは、func1の2つの異なるバージョンは文字通り、別の名前を持つ2つの別々の関数です。しかし、ソースコードレベルでは、どちらのバージョンもAcme::func1という名前を持っているように見えます。アプリケーション本体は問題なく、func1を呼び出すことができます。Acmeのバージョン1のヘッダを使用して呼び出しを行った場合、Acme::_1::func1が対応します。Acmeのバージョン2のヘッダを使用した場合、Acme::_2::func1が対応します。バージョン1のヘッダを使用したものの、すでに(直接または間接的に)Acme.1.dylibにリンクしていない場合、たとえ、Acme::_2::func1がプロセスに読み込まれていたとしても、Acme::_1::func1が見つからないので、リンクもしくは読み込み時に失敗します。

したがって、この方法は同じプロセスに複数のバージョンが含まれている場合でも、C++の型の安全を利用して、また、クライアントのソースコードにバージョン番号を含めることなく、ダイナミックリンカにバージョンの整合性を強制的に実現させます。

注:名前空間によるバージョニングの詳細については、次のサイトを参照してください。

先頭に戻る

どのstd C++シンボルが不変でどれが可変か(ABI問題)

「名前空間のusing宣言のためのstrong属性」「名前空間によるバージョン管理」を読んだ読者は、これがAppleのC++標準ライブラリの計画ではないかとすでに推測していることでしょう。時を経て、ABIの互換性を保たない変更がC++標準ライブラリに加えられた場合、前述のようにバージョニングを行うことで、古いABI互換バージョンと新しいABI非互換バージョンを同時に提供できるようにします。クライアントは、各自のニーズに応じてバージョンを選ぶことができます。そして、アプリケーションがたまたまサードパーティ製の共有ライブラリを読み込んだ場合でも、互換性のないstd::コンポーネントを暗黙のうちに混在することによる危険性から解放されます。

また、前述のAcmeの例において、ネストしたバージョニング名前空間の外側に1つのシンボル(func2)が残されていることに気づいたでしょうか。この例におけるfunc2は、バージョンが異なってもABIが変わらないことがAcmeによって保証されているシンボルです。

同様にAppleも、C++標準ライブラリのある小さなサブセットについては、今後C++標準ライブラリの新しいバージョンが出たとしてもABIを変更しないことを保証します。ABIが不変のシグネチャのサブセットは次のとおりです。

    namespace std {

    // rtti

    class type_info;

    // 例外

    class exception;
    class bad_exception;
    class bad_cast;
    class bad_typeid;
    class bad_alloc;
    class logic_error;
    class domain_error;
    class invalid_argument;
    class length_error;
    class out_of_range;
    class runtime_error;
    class range_error;
    class overflow_error;
    class underflow_error;

    // ハンドラ

    unexpected_handler set_unexpected(unexpected_handler) throw();
    void unexpected();
    terminate_handler set_terminate(terminate_handler) throw();
    void terminate();
    uncaught_exception() throw();
    struct nothrow_t {};
    new_handler set_new_handler(new_handler) throw();

    }  // std

    // new / delete

    void* operator new(std::size_t) throw(std::bad_alloc);
    void* operator new(std::size_t, const std::nothrow_t&) throw();
    void  operator delete(void*) throw();
    void  operator delete(void*, const std::nothrow_t&) throw();
    void* operator new[](std::size_t) throw(std::bad_alloc);
    void* operator new[](std::size_t, const std::nothrow_t&) throw();
    void  operator delete[](void*) throw();
    void  operator delete[](void*, const std::nothrow_t&) throw();
    void* operator new  (std::size_t, void*) throw();
    void* operator new[](std::size_t, void*) throw();
    void  operator delete  (void*, void*) throw();
    void  operator delete[](void*, void*) throw();

ABIが不変の例外クラスがあるのは特に重要です。なぜなら、例外は共有ライブラリの境界を簡単に越える傾向が高いからです。そして、もっとも頻繁にスローされるのは例外クラスだからです。ABIの不変性が保証されることで、標準ライブラリのどのバージョンにリンクしたコードも、ほかの任意のバージョンの標準ライブラリを使用する別のリンク対象ユニットによってスローされた標準例外クラスをキャッチできます。

先頭に戻る

最善のストリップ方法

リンクされたMach-Oバイナリには、グローバルとローカルの2種類のシンボルが含まれています。グローバルシンボルは、バインドするためにdyldによって使用され、実行時にdlsym()を使って検索可能です。ローカルシンボルは、アドレスに対応するシンボル名を示すときに、デバッガおよびCrashReporterによって使用されます。可視性が隠蔽の設定になっているC++の構成体はすべて、リンクされるとローカルシンボルになります。

製品を出荷する際にサイズを小さくするために、ローカルシンボルをストリップすることはごく一般的に行われています。XcodeはRelease構成においては、デフォルトでこれを行います。他方、グローバルシンボルをストリップすると、プログラムの実行時の意味付けが変わる可能性があるので、多少こつを必要とします。

ローカルシンボルを最短の時間でストリップする方法は、リンカに対して出力バイナリにシンボルを含めないように指示することです。それには、リンカに-xオプションを指定します。ローカルシンボル付きと、シンボルなしの2種類のバイナリを作成する場合、リンカにローカルシンボルを生成させ、プログラムのコピーを作成し、stripツールに-xオプションを指定して、コピーからローカルシンボルを削除します。

グローバルシンボルを制御するには、「可視性オプションの選択」のセクションで論じた4つの可視性オプションを使用します。

先頭に戻る

libstdc++のstringとwstringではなくbasic_stringインスタンス化の使用

可視性に隠蔽の設定をして、stringwstringではないstd::basic_stringを使用する際には、stringヘッダを、#pragma GCC visibility push(default)でラップしてインクルードする必要があります。これを行わないと、空文字列に関連した二重削除が発生します。これは、Appleのバグ(r. 4940079)と見なされており、今後のリリースで修正される予定です。

例:

    $ cat library.h

        #ifndef LIBRARY_H
        #define LIBRARY_H

        #include <string>

        typedef unsigned short uchar;
        typedef std::basic_string<uchar> ustring;

        __attribute__ ((visibility("default"))) ustring foo();

        #endif  // LIBRARY_H

    $ cat library.cpp

        #include "library.h"

        ustring foo()
        {
            ustring s;
            return s;
        }

    $ cat main.cpp

        #include "library.h"

        int main()
        {
            ustring s;
            s = foo();
        }

    $ export MallocBadFreeAbort=1
    $ g++ -fvisibility=hidden -dynamiclib -o library.so library.cpp
    $ g++ -fvisibility=hidden -o main main.cpp library.so
    $ ./main

        main(5585) malloc:***  Deallocation of a pointer not malloced: 0xc0ac; This
        could be a double free(), or free() called with the middle of an allocated
        block; Try setting environment variable MallocHelp to see tools to help debug
        Abort trap

修正するには、<library.h>を次のように変更します。

    $ cat library.h

        #ifndef LIBRARY_H
        #define LIBRARY_H

        #pragma GCC visibility push(default)   //変更追加
        #include <string>
        #pragma GCC visibility pop                 //変更追加

        typedef unsigned short uchar;
        typedef std::basic_string<uchar> ustring;

        __attribute__ ((visibility("default"))) ustring foo();

        #endif  // LIBRARY_H

先頭に戻る

C APIおよびC++による実装を使用した共有ライブラリのリリースごとのバージョンの互換性

共有ライブラリにおいて純粋なCインターフェイスを維持しながら、その共有ライブラリの実装においてC++を利用できます。

  • すべてのインターフェイス関数にextern「C」のリンクを与えます。

  • インターフェイスの一部ではないすべての関数に隠蔽の可視性を与えます。

  • インターフェイスのヘッダから実装の詳細をできるだけ排除します。

  • シンボルを意図どおりの可視性にするため、可視性を設定するコマンドラインオプションを使用することをクライアントに頼るのではなく、ヘッダ内のシンボルを修飾します。

  • nm -mgを使用して可視シンボルを確認します。未定義のシンボルは無視できます。

  • 共有ライブラリにおいて例外を取りこぼさないようにします。

  • new/deleteをオーバーライドしないようにします。オーバーライドする場合は、ライブラリ内のプライベートなものとします。

  • 静的ローカル変数を持つインターフェイスインライン関数は避けます。インライン関数には、隠蔽の可視性を与えます(たとえ、その関数がインターフェイスの一部だとしても)。

  • 意図的にABIを維持しないつもりでないかぎり、ライブラリから可視のシンボルを削除してはなりません。

  • ライブラリのメジャーバージョン番号をそのファイル名に組み込みます。そうすることで、たとえABIを維持しない場合でも、クライアントがライブラリのバージョンを選べます。

詳細および有用な提案については、Appleの『Dynamic Library Design Guidelines』の参考文献を参照してください。

先頭に戻る

C++ APIおよびC++による実装を使用した共有ライブラリのリリースごとのバージョンの互換性

共有ライブラリは、C++インターフェイスを持つこともできます。前のセクションで列挙したポイントのほかに、ABIの不変性を維持するためには次の点も考慮する必要があります。

  • ABI問題のセクションで詳述したシンボルを除き、インターフェイスの中ではstd C++ライブラリを避けます。ただし、自分の実装の中ではstd C++ライブラリを自由に使用できます。

  • 共有ライブラリから例外を伝播させる場合、またはクライアントがdynamic_cast可能な型がある場合、必ずそれらの型を可視にします。

  • インライン関数からアクセスされないプライベートメンバ関数には、隠蔽の可視性を与えることを検討します。

  • クライアントからプライベートデータや関数を隠すために、できるだけPimplイディオムを使用することを検討します。

  • 名前空間によるバージョニングを使用して、いずれ生じるABIの非互換性による問題を軽減することを検討します。

先頭に戻る

ドキュメント改訂履歴

日付メモ
2007-01-25初版

掲載日: 2007-01-25




Did this document help you?
Yes: Tell us what works for you.

It’s good, but: Report typos, inaccuracies, and so forth.

It wasn’t helpful: Tell us what would have helped.