名前修飾名前修飾(なまえしゅうしょく、英: name mangling)は、現代的なコンピュータプログラミング言語処理系で用いられている手法で、サブルーチン(関数)名などに対する内部名を、その表層的な名前のみならず、関数であればその引数の型や返戻値の型などといった意味的な情報を含めて修飾した(manglingした)名前とするものである。コンパイラからリンカ、さらには実行時のデバッガなども含んだシステム全体が、高度な型に関する情報などをサポートするように再実装するには多くの難しさがあるが、この手法であれば、システムの多くの部分ではわずかな修正(たとえば、名前に '$' という文字が含まれることを許すようにする、など)で済む。特に、多重定義(オーバーロード)を許す言語では、同一の表層名に対して許される多重定義や、その同定について上手に修飾を設計すれば、扱いが単純になる。また、そのままではエラーメッセージ等が読み辛いものとなるが、「解読」ルーチンを呼ぶように修正するだけで、型の情報などが付加された、むしろわかりやすいメッセージが出力されるようになる。 Microsoft Windowsの場合一般的なC・Pascalなどの言語は関数の多重定義をサポートせず、名前修飾を必要としないが、場合によっては名前修飾によって関数についての情報を付加することがある。 例えば、Microsoft Windows 上のコンパイラは複数の呼出規約(サブルーチンとデータをやりとりする方法)をサポートしている。呼出規約の間には互換性がないので、コンパイラは名前修飾によって呼出規約を詳細に記述する。 マイクロソフト (MS) によって確立された名前修飾のスキームがあり、非公式に他のコンパイラもこれに従っている。例えば、Digital Mars・C++Builder(ボーランドC)・gccである。このスキームは他の言語、例えば、Pascal・D言語・Delphi・FORTRAN・C#[要出典]にも適用される。このようにして、それら処理系のデフォルトの呼出規約が異なる場合も、それら処理系で作成したサブルーチンが現存のWindows ライブラリを呼んだり、そこから呼ばれたりすることができる。 次のCコードをコンパイルするとしよう: int _cdecl f(int x) { return 0; }
int _stdcall g(int y) { return 0; }
int _fastcall h(int z) { return 0; }
32bit コンパイラはそれぞれ、以下を出力する。 _f
_g@4
@h@4
他の一般的な修飾法として、( C++の場合名前修飾を行う処理系のうち、C++コンパイラは最も広く用いられているが、最も標準化が進んでいないものである。最初のC++コンパイラはCソースコードへのトランスレータとして実装された。そのため、シンボルの名前はCの識別子の規則に従う必要があった。後にC++コンパイラ自身が機械語コードやアセンブラコードを出力するようになっても、計算機システムのリンカは総じてC++のシンボルをサポートせず、名前修飾が必要な状態が続いた。 C++言語は、標準的な修飾規則を定めていない。そのため、コンパイラによって修飾規則が異なる。(クラス、テンプレート、演算子オーバーロードなどの情報を格納する)C++の修飾がかなり複雑になりうることも加わり、異なるコンパイラのオブジェクトコードはリンクすることができないのが通常である。 簡単な例C++における int f (void) { return 1; }
int f (int) { return 0; }
void g (void) { int i = f(), j = f(0); }
この二つは異なった関数である。名前以外に全く関係がない。馬鹿正直にこれをCに変換すると、Cコンパイラはエラーを吐く。Cでは関数の名前の重複は許されないからである。そこで、C++コンパイラはシンボル名に型情報を加える。例えばこんな風になるだろう: int __f_v (void) { return 1; }
int __f_i (int) { return 0; }
void __g_v (void) { int i = __f_v(), j = __f_i(0); }
ここで、 複雑な例もっと複雑な例を挙げる。実際に用いられている名前修飾を見てみよう。GNU GCC 3.x系は次のクラス例をどのように修飾するだろうか。修飾されたシンボルはそれぞれの識別子の下に表示されている。 namespace wikipedia {
class article {
public:
std::string format (void);
/* = _ZN9wikipedia7article6formatEv */
bool print_to (std::ostream&);
/* = _ZN9wikipedia7article8print_toERSo */
class wikilink {
public:
wikilink (std::string const& name);
/* = _ZN9wikipedia7article8wikilinkC1ERKSs */
};
};
}
ここでの名前修飾スキームは比較的単純である。修飾された名前は全て _ZN·9wikipedia·7article·6format·E となる。 関数の場合は、続いて型情報が付加される。 _ZN·9wikipedia·7article·6format·E·v となる。
_ZN·9wikipedia·7article·8print_to·E·RSo コンパイラによる名前修飾の相違C++では、ささいな識別子ですら、名前修飾の標準スキームは存在しない。そのため、コンパイラベンダによって、あるいは同じコンパイラでも版によって、更に場合によっては同じ版でもプラットフォームによって全く異なった、互換性のない方法をとることになる。同じ関数について、その違いを見てみよう。
注:
C++からリンクする際のCシンボルの扱い次のようなよくあるC++の例 #ifdef __cplusplus
extern "C" {
#endif
/* ... */
#ifdef __cplusplus
}
#endif
は、引き続くシンボルを修飾しないことを指示する。すなわち、コンパイラはあたかもCコンパイラであるかのように、修飾なしの名前を用いたバイナリを吐く。Cが名前修飾を利用していないので、C++コンパイラもそれらの識別子を参照する際に名前修飾を避けなければならない。 例として、標準的な文字列ライブラリ #ifdef __cplusplus
extern "C" {
#endif
void *memset (void *, int, size_t);
char *strcat (char *, const char *);
int strcmp (const char *, const char *);
char *strcpy (char *, const char *);
#ifdef __cplusplus
}
#endif
そこで、次のコード if (strcmp(argv[1], "-x") == 0)
strcpy(a, argv[2]);
else
memset(a, 0, sizeof(a));
は、正しい、修飾されない if (__1cGstrcmp6Fpkc1_i_(argv[1], "-x") == 0)
__1cGstrcpy6Fpcpkc_0_(a, argv[2]);
else
__1cGmemset6FpviI_0_(a, 0, sizeof(a));
これらのシンボルはCのランタイムライブラリ(例えば libc)には存在しないので、リンカはエラーを報告することになる。 C++での名前修飾の標準化C++で名前修飾の標準化を行うと、実装をまたいだ運用がしやすくなるというのが比較的広く信じられているが、これは実際には正しくない。名前修飾はアプリケーションバイナリインタフェース (ABI) や他の細かな言語仕様(例外処理、仮想関数テーブルのレイアウト、構造体のパディングなど)におけるいくつかの問題の一つに過ぎず、名前修飾だけをどうかしても非互換性は残ることになる。更に、特定の修飾法を決めてしまうと、実装が制限されるシステムが出現しうる(例えば、シンボルの長さ)。また、名前修飾を標準化してしまうと、例えばC++の文法を理解できるリンカのような、名前修飾を必要としない実装を妨げる可能性もある。 そのため、ISOではC++の標準 (en:ISO/IEC 14882) として、名前修飾を標準化することを特に目指してはいない。逆に、Annotated C++ Reference Manual(ARM の名でも知られる。 ISBN 0-201-51459-1, section 7.2.1c)では、ABI上の他の非互換性を抱えたモジュールを誤ってリンクしないように、異なった名前修飾法を用いることが推奨されている。 C++名前修飾問題の現実的な影響C++のシンボルはDLLや共有オブジェクト[要説明]を通してルーチン的にエクスポートされるため、名前修飾スキームはコンパイラの問題だけではすまなくなる。ライブラリをコンパイルするにあたって、複数のコンパイラ(場合によっては、同じコンパイラでも版が異なるだけで問題になりうる)によって名前修飾がそれぞれ異なったスキームで行われると、それらのライブラリを参照する際、しばしばシンボルが解決できなくなってしまう。例えば、複数のC++コンパイラ(例えば、GCCとOS付属のコンパイラ)が導入されているシステムにBoost C++ライブラリを導入しようとすると、二度それをコンパイルしなければならない(GCCで1回、OS付属のコンパイラで1回)。 このため、名前修飾はC++が関係したABIでの重要な側面の一つとなっている。 Javaの場合Javaでは、言語、コンパイラ、.classファイルフォーマットが同時に設計され、また開発当初からオブジェクト指向が取り入れられていたため、名前修飾を必要とするような問題はJava実行時環境 (JRE) の実装には存在しない。しかしながら、これまでに見てきた名前修飾に類似した名前の変換が必要な場合がある。 内部クラスおよび無名クラスに一意名を与える内部クラス (inner class) のスコープは、その親クラスに制限される。そのため、コンパイラは「修飾付きの」 (qualified) パブリックな名前を内部クラスに対して与えなければならない。同様に無名(匿名)クラス (anonymous class) には「偽の」パブリックな名前を生成しなければならない(無名クラスはコンパイラの概念であり、実行時には関係がない)。そこで、次のJavaプログラムをコンパイルすると public class Foo {
// 内部クラス。
class bar {
public int x;
}
public void zark() {
// 無名クラスのインスタンス化。
Object f = new Object() {
public String toString() {
return "hello";
}
};
}
}
3つの .class ファイルが生成される。
ドル記号 ($) はJava仮想マシン (JVM) の仕様上許されているので、これら3つのクラス名は全て有効であり、Java言語の仕様上 $ は通常のJavaクラス定義に用いることができないので、コンパイラは安全にこれらの名前を利用することができる。 完全修飾名は特定のクラスローダインスタンスの内部でのみ一意であるので、実行時にはJavaにおける名前の解決は更に複雑である。クラスローダは階層性をもっており、JVMの各スレッドはいわゆる文脈クラスローダ (context class loader) を持っている。そこで、2つの異なったクラスローダインスタンスが同じ名前のクラスを含むとき、システムは初めルート(あるいはシステム)クラスローダを用いてクラスをロードしようとし、次いで階層に従って文脈クラスローダをたどる。 Java Native InterfaceJava Native Interface (JNI) はJavaとネイティブコードを双方向に相互運用するための標準仕様である。Javaのネイティブメソッドサポートによって、Javaで記述されたプログラムから他の言語(通常CまたはC++)で書かれたプログラムを呼ぶことができる。ここでは2つの名前解決に関する懸念があるが、いずれも特に標準的な作法で実装されてはいない。 これとは別に、Java Native Access (JNA) は、Javaプログラムからネイティブの共有ライブラリにアクセスする方法をライブラリレベルで提供する。 Pythonの場合Pythonのプログラマは識別子の最初の2文字をアンダースコアにすることで、明示的にそれが「プライベートな名前」(スコープがクラスに限られる)であることを示すことができる。Pythonコンパイラはこれらに遭遇すると、1個のアンダースコアとその識別子を包含するクラスの名前を先頭に追加することで、プライベートな名前を大域的なシンボルに変換する。例えばPython 2.xでは、 class Test:
def __privateSymbol(self):
pass
def normalSymbol(self):
pass
print dir(Test)
は次のようになる。 ['_Test__privateSymbol', '__doc__', '__module__', 'normalSymbol'] Turbo Pascal / Delphi の場合これらのPascal処理系では、次のようにして名前修飾を抑制する。 exports myFunc name 'myFunc', myProc name 'myProc'; Objective-Cの場合Objective-Cのメソッドは、本質的に二種類に分けられる。一つは クラス(「静的」)メソッドで、もう一つはインスタンスメソッドである。Objective-Cでのメソッド宣言は次のような形式である。 + method name: argument name1:parameter1 ... - method name: argument name1:parameter1 ... クラスメソッドは + で示される。インスタンスメソッドは - で示される。典型的なクラスメソッド宣言は、次のようになるだろう + (id) initWithX: (int) number andY: (int) number; + (id) new; インスタンスメソッドならば、次のようである - (id) value; - (id) setValue: (id) new_value; それぞれのメソッド宣言は特有の内部表現を持っている。コンパイル時に、メソッド名は次のスキームによって変換される。クラスメソッドでは _c_Class_methodname_name1_name2_ ... となり、インスタンスメソッドでは _i_Class_methodname_name1_name2_ ... となる。 Objective-Cのコロンは下線に変換される。そこで、Point クラスに属するクラスメソッド + (id) initWithX: (int) number andY: (int) number; は次のように変換されるだろう _c_Point_initWithX_andY_。同じクラスに属するインスタンスメソッド - (id) value; は _i_Point_value となる。 クラスの各メソッドはこのようにラベルされるが、全てのメソッドがこのように表現された場合、あるクラスが応答すべきメソッドを探し出すのは面倒な作業となりうる。そのため、各々のメソッドに整数のようなシンボルを一意に割り当てる。このようなシンボルは「セレクタ」として知られる。Objective-Cでは、プログラマがセレクタを直接管理することができる — Objective-Cではそれらに特別の型を与えている — SEL。 コンパイル中に、(_i_Point_valueのような)文字による表現からセレクタ(SEL型)へのマップが作成される。文字による表現を操作するよりもセレクタを管理する方がメソッドを効果的に扱うことができる。セレクタがマッチするのはメソッドの名前だけであり、それが属するクラスではないということに注意してほしい。クラスが異なれば同じ名前のメソッドでも実装が異なることがある。このため、メソッドの実装にも特別の識別子が与えられる — 実装ポインタ (implementation pointer) と呼ばれ、IMP 型を持つ。 オブジェクトにメッセージを送ると、それはコンパイラによって、 id objc_msgSend(id receiver, SEL selector, ...) 関数ないしはその従兄弟のどれかに対する呼び出しとしてエンコードされる。ここで、receiverはそのメッセージの受け手であり、SELによって呼び出されるメソッドが決まる。各々のクラスはそれ自身の表を持っており、セレクタと実装 — メソッドの実体が存在するメモリ空間を指定する実装ポインタ — との相互対照ができるようになっている。また別の表にはクラスとインスタンスメソッドが記録される。SELからIMPへの対照表に格納されることはさておき、関数は本質的に無名である。 あるセレクタに対するSELの値はクラスによって変わることがなく、多態性を実現している。 Objective-Cの実行環境はメソッドの引数と返り値の型についての情報を保持しているが、メソッドの名前の一部として保持されるわけではなく、クラスによって変化しうる。 Objective-Cは名前空間をサポートしないので、クラス名を修飾する必要はない。 脚注関連項目
外部リンク
|