名前修飾

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動: 案内検索

名前修飾(なまえしゅうしょく)は、現代的なプログラミング言語処理系で用いられる手法である。プログラム内の各種実体に一意の名前を与えることで、様々な問題を解決する。

この手法を用いると、サブルーチン(関数)、構造体クラス他のデータ型に付加的な情報を追加し、より多くの意味論的な情報をコンパイラからリンカへ渡すことができるようになる。

このような付加的情報は、プログラミング言語が名前空間(モジュール、クラス、あるいは明示的な namespace 指令で示される)さえ異なれば、異なる実体に同一の識別子を与えることを許す場合に必要になってくる。

コンパイラが生成したオブジェクトコードは、リンカによって通常他のオブジェクトコード(生成するのに同一のコンパイラを用いる場合も異なるコンパイラを用いる場合もある)とリンクされる。リンカはプログラム内の各実体について多くの情報を必要とする。例えば、関数を正しくリンクするためには関数の名前、引数の数、引数の型、他の情報が必要である。

Microsoft Windowsの場合[編集]

一般的なCPascalなどの言語は関数の多重定義をサポートせず、名前修飾を必要としないが、場合によっては名前修飾によって関数についての情報を付加することがある。

例えば、Microsoft Windows 上のコンパイラは複数の呼出規約(サブルーチンとデータをやりとりする方法)をサポートしている。呼出規約の間には互換性がないので、コンパイラは名前修飾によって呼出規約を詳細に記述する。

MSによって確立された名前修飾のスキームがあり、非公式に他のコンパイラもこれに従っている。例えば、Digital Marsボーランドgccである。このスキームは他の言語、例えば、PascalD言語DelphiFORTRANC#にも適用される。このようにして、それら処理系のデフォルトの呼出規約が異なる場合も、それら処理系で作成したサブルーチンが現存のWindows ライブラリを呼んだり、そこから呼ばれたりすることができる。

次のCコードをコンパイルするとしよう:

int _cdecl    f(int x) { return 0; }
int _stdcall  g(int y) { return 0; }
int _fastcall h(int z) { return 0; }

_cdecl はCの標準の呼び出し規約である。

32 bit コンパイラはそれぞれ、以下を出力する。

_f
_g@4
@h@4

stdcallfastcallでは、関数名は_名前@X@名前@Xのようにエンコードされる。Xにはコールスタック(以下単にスタック)に積まれる引数のバイト数が入る。

他の一般的な修飾法は、前置句 (prefix) を、下線付きで(__func__のように)あるいは標準的な大文字化を行ったりして加えるものである。

C++の場合[編集]

名前修飾を行う処理系のうち、C++コンパイラは最も広く用いられているが、最も標準化が進んでいないものである。最初のC++コンパイラはCソースコードへのトランスレータとして実装された。そのため、シンボルの名前はCの識別子の規則に従う必要があった。後にC++コンパイラ自身が機械語コードやアセンブラコードを出力するようになっても、計算機システムのリンカは総じてC++のシンボルをサポートせず、名前修飾が必要な状態が続いた。

C++言語は、標準的な修飾規則を定めていない。そこで、コンパイラによって修飾規則が異なる。C++の修飾がかなり複雑になりうること(クラス、デフォルトの引数、変数のオーナー、演算子オーヴァーロードなどの情報を格納する)も加わり、異なるコンパイラのオブジェクトコードはリンクすることができないのが通常である。

簡単な例[編集]

C++におけるf()の、次の二つの定義をみてみよう:

int f (void) { return 1; }
int f (int)  { return 0; }
void g (void) { int i = f(), j = f(0); }

この二つは異なった関数である。名前以外に全く関係がない。馬鹿正直にこれを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); }

ここで、g()は名前の重複問題がないのに修飾されていることに注意して欲しい。修飾は全てのシンボルに適用される。

複雑な例[編集]

もっと複雑な例を挙げる。実際に用いられている名前修飾を見てみよう。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 */
      };
   };
}

ここでの名前修飾スキームは比較的単純である。修飾された名前は全て _Z で始まる。下線に大文字を続けたものはCおよびC++では予約語であることに注意されたい。従って、ユーザの識別子とぶつかることはない。ネストされた名前は(名前空間、クラス両者)、Nをつけ、次いで <長さ,id> のペアをつけて示す(「長さ」は次の識別子の長さ)。最後にEを付ける。例えば、wikipedia::article::format

_ZN·9wikipedia·7article·6format·E  

となる。

関数の場合は、続いて型情報が付加される。format()void 関数なので、単にvを付ける。よって、

_ZN·9wikipedia·7article·6format·E·v

となる。

print_toの場合は、標準的な型として std::ostream (あるいは、もっと正確には std::basic_ostream<char, char_traits<char> >)が用いられ、これには特殊な別名 So がある。よって、この型に対する参照はRSoとなる。名前の完成形はこうなる。

_ZN·9wikipedia·7article·8print_to·E·RSo

コンパイラによる名前修飾の相違[編集]

C++では、ささいな識別子ですら、名前修飾の標準スキームは存在しない。そのため、コンパイラベンダによって、あるいは同じコンパイラでも版によって、更に場合によっては同じ版でもプラットフォームによって全く異なった、互換性のない方法をとることになる。同じ関数について、その違いを見てみよう。

コンパイラ void h(int) void h(int, char) void h(void)
GNU GCC 3.x _Z1hi _Z1hic _Z1hv
GNU GCC 2.9x h__Fi h__Fic h__Fv
Intel C++ 8.0 for Linux _Z1hi _Z1hic _Z1hv
Microsoft VC++ v6/v7 ?h@@YAXH@Z ?h@@YAXHD@Z ?h@@YAXXZ
Borland C++ v3.1 @h$qi @h$qizc @h$qv
OpenVMS C++ V6.5 (ARM mode) H__XI H__XIC H__XV
OpenVMS C++ V6.5 (ANSI mode) CXX$__7H__FI0ARG51T CXX$__7H__FIC26CDH77 CXX$__7H__FV2CB06E8
OpenVMS C++ X7.1 IA-64 CXX$_Z1HI2DSQ26A CXX$_Z1HIC2NP3LI4 CXX$_Z1HV0BCA19V
Digital Mars C++ ?h@@YAXH@Z ?h@@YAXHD@Z ?h@@YAXXZ
SunPro CC __1cBh6Fi_v_ __1cBh6Fic_v_ __1cBh6F_v_
HP aC++ A.05.55 IA-64 _Z1hi _Z1hic _Z1hv
HP aC++ A.03.45 PA-RISC h__Fi h__Fic h__Fv
Tru64 C++ V6.5 (ARM mode) h__Xi h__Xic h__Xv
Tru64 C++ V6.5 (ANSI mode) __7h__Fi __7h__Fic __7h__Fv

註:

  • OpenVMS VAX、Alpha (IA-64を除く)及び Tru64 上のCompaq C++ コンパイラは二種類の修飾スキームを持っている。標準化前のもともとのスキームは ARM モデルとして知られていた。これはC++ Annotated Reference Manual (ARM) に記述された方法を元にしている。標準C++の機能拡充、特にテンプレート機能の追加に伴い、ARM は次第に旧式化していった — ある種の関数型をエンコードできず、異なった関数に異なったシンボル名を割り当てることができなくなっていた。そこで、より新しい ANSI モデルが導入され、ANSIテンプレート機能が全て利用可能になったが、過去の版との互換性は失われた。todo: the different isn't obvious from the examples. maybe a template or something should be added...
  • 標準的なApplication Binary InterfaceであるIA-64が存在する(#外部リンク参照)。これは(他にもあるが)標準的な名前修飾スキームを定義した者であり、全てのIA-64コンパイラで利用されている。加えて、GNU GCC 3.xもこの標準を用いている。インテル環境以外でも利用することができる。


C++からリンクする際の C シンボルの扱い[編集]

次のようなよくあるC++の例

#ifdef __cplusplus 
extern "C" {
#endif
    /* ... */
#ifdef __cplusplus
}
#endif

は、引き続くシンボルを修飾しないことを指示する。すなわち、コンパイラはあたかもCコンパイラであるかのように、修飾なしの名前を用いたバイナリを吐く。Cが名前修飾を利用していないので、C++コンパイラもそれらの識別子を参照する際名前修飾を避けなければならない。

例として、標準的な文字列ライブラリ <string.h> は、通常次のようなコードを含む:

#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));

は、正しい、修飾されない strcmp 及び memset を用いることになる。extern が用いられなければ、C++コンパイラは同等の次のコードを生成するだろう

if (__1cGstrcmp6Fpkc1_i_(argv[1], "-x") == 0) 
    __1cGstrcpy6Fpcpkc_0_(a, argv[2]);
else 
    __1cGmemset6FpviI_0_(a, 0, sizeof(a));

これらのシンボルはCのランタイムライブラリ(例えば libc)には存在しないので、リンカはエラーを報告することになる。

C++での名前修飾の標準化[編集]

C++で名前修飾の標準化を行うと、実装をまたいだ運用がしやすくなるというのが比較的広く信じられているが、これは実際には正しくない。名前修飾はApplication Binary Interface (ABI) や他の細かな言語仕様(例外処理仮想テーブルのレイアウト、構造体のパディングなど)におけるいくつかの問題の一つに過ぎず、名前修飾だけをどうかしても非互換性は残ることになる。更に、特定の修飾法を決めてしまうと、実装が制限されるシステムが出現しうる(例えば、シンボルの長さ)。また、名前修飾を標準化してしまうと、例えばC++の文法を理解できるリンカのような、名前修飾を必要としない実装を妨げる可能性もある。

そのため、ISOではC++の標準 (en:ISO/IEC 14882) として、名前修飾を標準化することを特に目指してはいない。逆に、Annotated C++ Reference ManualARM の名でも知られる。 ISBN 0-201-51459-1, section 7.2.1c)では、ABI上の他の非互換性を抱えたモジュールを誤ってリンクしないように、異なった名前修飾法を用いることが推奨されている。

C++名前修飾問題の現実的な影響[編集]

C++のシンボルはDLL共有オブジェクトを通してルーチン的にエクスポートされるため、名前修飾スキームはコンパイラの問題だけではすまなくなる。ライブラリをコンパイルするにあたって、複数のコンパイラ(場合によっては、同じコンパイラでも版が異なるだけで問題になりうる)によって名前修飾がそれぞれ異なったスキームで行われると、それらのライブラリを参照する際、しばしばシンボルが解決できなくなってしまう。例えば、複数のC++コンパイラ(例えば、GCCとOS付属のコンパイラ)が導入されているシステムにBoostを導入しようとすると、二度それをコンパイルしなければならない ―― OSベンダのコンパイラとGCCで。

このため、名前修飾はC++が関係した ABI での重要な側面の一つとなっている。

Java の場合[編集]

言語、コンパイラ、.classファイルフォーマットが同時に設計され、また開発当初からオブジェクト指向が取り入れられていたため、名前修飾を必要とするような問題はJava実行時環境の実装には存在しない。しかしながら、これまでに見てきた名前修飾に類似した名前の変換が必要な場合がある。

内部及び無名クラスに一意名を与える[編集]

無名の (anonymous) クラスのスコープは、その親クラスに制限される。そのため、コンパイラは「修飾付きの」 (qualified) パブリックな名前を内部クラスに対して与えなければならない。同様に無名のクラスには「偽の」パブリックな名前を生成しなければならない(無名クラスはコンパイラの概念であり、実行時には関係がない)。そこで、次のJavaプログラムをコンパイルすると

public class Foo {
    class bar {
        public int x;
    }
 
    public void zark () {
        Object f = new Object () {
            public String toString() {
                return "hello";
            }
        };
    }
}

三つの .class ファイルが生成される。

  • Foo.class, 主クラス(外側のクラス) Foo を含む。
  • Foo$bar.class, Foo.bar という名前付きの内部クラスを含む。
  • Foo$1.class, メソッド Foo.zark に対して局所的な無名の内部クラスを含む。

ドル記号 ($) はJVMの仕様上許されているので、これら三つのクラス名は全て有効であり、Java言語の仕様上 $ は通常のJavaクラス定義に用いることができないので、コンパイラは安全にこれらの名前を利用することができる。

完全修飾名は特定のクラスローダインスタンスの内部でのみ一意であるので、実行時にはJavaにおける名前の解決は更に複雑である。クラスローダは階層性をもっており、JVMの各スレッドはいわゆる文脈クラスローダ (context class loader) を持っている。そこで、二つの異なったクラスローダインスタンスが同じ名前のクラスを含む時、システムは初めルート(あるいはシステム)クラスローダを用いてクラスをロードしようとし、次いで階層に従って文脈クラスローダをたどる。

Java - ネイティブ間のインターフェースの扱い[編集]

Javaのネイティブメソッドサポートによって、Javaで記述されたプログラムから他の言語で書かれたプログラム(通常CまたはC++)を呼ぶことができる。ここでは二つの名前解決が関係するが、標準的な方法は存在しない。

Python の場合[編集]

Pythonのプログラマは識別子の最初の二文字を下線にすることで、明示的にそれが「プライベートな名前」(スコープがクラスに限られる)であることを示すことができる。コンパイラはこれらを見るとプライベートな名前を大域的なシンボルに置き換える。これは、その識別子を包含するクラスの名前を一個の下線とともに前づけして行われる。例えば、

class Test:
    def __privateSymbol():
        pass
    def normalSymbol():
        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は名前空間をサポートしないので、クラス名を修飾する必要はない。

外部リンク[編集]