|
はじめにこのドキュメントの目的は、最新の複雑なC++ Mach-OアプリケーションをMac OS Xに導入する作業を容易にすることです。よく生じるいくつかの問題に対して、役に立ついくつかのヒントを示します。網羅的なリファレンスといえるものではありませんが、悩めるC++デベロッパを正しい方向に導き、必要に応じてさらに詳細な情報を入手する手だてを提供します。 可視性オプションの選択GCCでは、可視性とは、ほかのツールでいうダイナミックライブラリのインポート/エクスポートのことです。ただし、GCCのシンボルは可視か隠蔽されているかのどちらかしかありません。可視シンボルは、共有ライブラリのエントリポイントとなります。詳細については、「Controlling Symbol Visibility」を参照してください。ここでは、概要を述べるに留めます。 Cでは、どのシンボルを可視にし、どれを隠蔽するかを決めるのは比較的簡単です。共有ライブラリのエントリポイントは、可視であることが必須です。そのほかはすべて隠蔽できます。C++では、多くの定義はソースファイルではなくヘッダファイルに含まれます。C++のヘッダファイルには、インターフェイスとプライベート実装の詳細を含めることができます。たとえば、テンプレート(クラスと関数の両方)、クラス宣言、インライン関数(メンバと非メンバの両方)などです。 可視性を宣言する方法は4つあります。
上記のそれぞれの方法の役割と、それらが相互にどのように作用するのかを理解することが大切です。はじめの3つの方法は、コンパイル時に可視性を宣言します。4番目はリンク時の操作です。エクスポートリストは、コンパイラが可視とマークしたシンボルを隠蔽することができますが、コンパイラが隠蔽するものとしてとマークしたシンボルを、エクスポートリストを使って可視にすることはできません。 可視性のコマンドラインオプション可視性コマンドラインオプションは、可視性をデフォルト設定する際に役立ちます。特に、サードパーティのソースコードや、何らかの理由で属性またはプラグマで修飾することが実用的ではないソースコードの場合に役に立ちます。コマンドライン可視性オプションを指定しない場合、シンボルは可視とマークされます(ただし、ほかの3つの方法のいずれかでオーバーライドされないかぎり)。デフォルトの可視性を明示的に選択するには、次のオプションをコマンドラインに指定します。
-fvisibility=default
コマンドラインに 便利と考えられる可視性に関連するコマンドオプションは、このほかに2つあります。
プラグマを使用した可視性の設定プラグマを使って、個々のシンボルまたはシンボルのグループの可視性を制御できます。そのようにプラグマを使うことで、可視性コマンドラインオプションとは関係なく、影響を受けるシンボルの可視性を制御できます。その結果、保守の観点から、ソースコードの堅牢性が増す可能性があります。 コンパイラのデフォルトモードを可視の設定にするには、次のように指定します。 #pragma GCC visibility push(default) コンパイラのデフォルトモードを隠蔽の設定にするには、次のように指定します。 #pragma GCC visibility push(hidden) 直前に行ったGCC visibility pushプラグマを解除するには、次のように指定します。 #pragma GCC visibility pop これによって、可視性プラグマの対象範囲が形成されます。対象範囲はネストすることが可能であり、最も内側の対象範囲が優先されます。 注:よくある誤りの1つは、プラグマの綴り間違いです。この場合、 コンパイラの型に関するメタデータ(
#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() {}
次のように
class PUBLIC MyClass {/*...*/};
属性は、 エクスポートリストを使用したシンボルの隠蔽このテクニカルノートで紹介する最後のツールは、エクスポートリストです。これは単純に、どのシンボルを隠蔽するべきかをリンカに指示する(マングル形式でなければならない)シンボルのリストです。形式は2種類あります。
シンボルの可視性のまとめ上記の可視性ツールを組み合わせて使うと便利な場合があります。たとえば、隠蔽すべき多数のシンボルと、可視にすべき少数のシンボルがライブラリにある場合、 共有ライブラリの境界を越えて
// 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テンプレートが用意されています。
アプリケーションテンプレートCarbonアプリケーションテンプレートのいずれか1つを選び、Carbon APIに基づく、たたき台となるC++アプリケーションを作成します。たたき台のアプリケーションは、通常のウインドウコマンド(新規、閉じる、最小化など)に応答するOS Xアプリケーションウインドウを表示します。このテンプレートには、「About」ウインドウの例もあります。テンプレートは、C++のスタイルで、イベントをキャッチしてカスタマイズする方法、およびnibファイルから読み取りを行う方法も示します。 「Carbon C++ Application」と「Carbon C++ Standard Application」の唯一の違いは、デフォルトの可視性の設定です。「Standard」(標準)のテンプレートは、デフォルトで可視に設定されていますが、もう一方のテンプレートは隠蔽に設定されます。デフォルトで隠蔽の可視性を選んだ場合、共有ライブラリにリンクするとき、共有ライブラリの境界を越えて知られている必要のあるシンボルを、属性修飾またはプラグマを使って明示的に可視として宣言する必要があります。これには、 汎用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つの非配列形式のみを置き換える場合は、デフォルトの配列形式から、置き換えた非配列形式に転送があるものと想定してください。 置き換えた項目は、アプリーション全体に渡って有効です。ほかのリンク対象ユニット(共有ライブラリ)のコードも、置き換えられた 一般的に、それが共有ライブラリの唯一の目的でなければ、共有ライブラリにおいてこれらの演算子をオーバーライドするべきではありません。そうでなければ、アプリケーションはオーバーライドされた ごくまれな状況で、共有ライブラリにこれらの演算子のプライベートな定義があると便利な場合があります。これは、
__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
そうすることで、メモリ所有権がこの共有ライブラリに移転したり、ほかへ移転したりしないことを作者が保証する必要があります。メモリ所有権の移転は、参照をカウントするオブジェクト( 現在のツールセットにはバグがあります。それによって、変換ユニットがこれらの演算子を置き換え、弱いリンク(たとえば、インライン関数や暗黙のテンプレートのインスタンス化)を持つシンボルを含まない場合、リンカは置き換えられた演算子
__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
これは、ほとんどのクライアントコードで機能します。理由は、クライアントコードはネストした名前空間を知らなくてもよく、 error: specialization of 'template<class T> class Acme::_1::V' in different namespace すなわち、クライアントは error: 'func2' was not declared in this scope このエラーは、クライアントのコード内にある両方の func2(v_int); func2(v_mytype); この時点で GCCコンパイラには、名前空間がネストしていることをクライアントが認識する必要なくこのコードを機能させる拡張があります。 using namespace _1 __attribute__((__strong__)); これによって、クライアントの元のコードは、 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内の隠されている名前空間は、 したがって、この方法は同じプロセスに複数のバージョンが含まれている場合でも、C++の型の安全を利用して、また、クライアントのソースコードにバージョン番号を含めることなく、ダイナミックリンカにバージョンの整合性を強制的に実現させます。 注:名前空間によるバージョニングの詳細については、次のサイトを参照してください。 どのstd C++シンボルが不変でどれが可変か(ABI問題)「名前空間のusing宣言のためのstrong属性」と「名前空間によるバージョン管理」を読んだ読者は、これがAppleのC++標準ライブラリの計画ではないかとすでに推測していることでしょう。時を経て、ABIの互換性を保たない変更がC++標準ライブラリに加えられた場合、前述のようにバージョニングを行うことで、古いABI互換バージョンと新しいABI非互換バージョンを同時に提供できるようにします。クライアントは、各自のニーズに応じてバージョンを選ぶことができます。そして、アプリケーションがたまたまサードパーティ製の共有ライブラリを読み込んだ場合でも、互換性のない また、前述のAcmeの例において、ネストしたバージョニング名前空間の外側に1つのシンボル( 同様に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種類のシンボルが含まれています。グローバルシンボルは、バインドするために 製品を出荷する際にサイズを小さくするために、ローカルシンボルをストリップすることはごく一般的に行われています。XcodeはRelease構成においては、デフォルトでこれを行います。他方、グローバルシンボルをストリップすると、プログラムの実行時の意味付けが変わる可能性があるので、多少こつを必要とします。 ローカルシンボルを最短の時間でストリップする方法は、リンカに対して出力バイナリにシンボルを含めないように指示することです。それには、リンカに グローバルシンボルを制御するには、「可視性オプションの選択」のセクションで論じた4つの可視性オプションを使用します。 libstdc++のstringとwstringではなくbasic_stringインスタンス化の使用可視性に隠蔽の設定をして、 例:
$ 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
修正するには、
$ 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++を利用できます。
詳細および有用な提案については、Appleの『Dynamic Library Design Guidelines』の参考文献を参照してください。 C++ APIおよびC++による実装を使用した共有ライブラリのリリースごとのバージョンの互換性共有ライブラリは、C++インターフェイスを持つこともできます。前のセクションで列挙したポイントのほかに、ABIの不変性を維持するためには次の点も考慮する必要があります。
ドキュメント改訂履歴
掲載日: 2007-01-25 | ||||||||||
|