名前修飾

名前修飾は、現代的なコンピュータプログラミング言語処理系で用いられている手法で、サブルーチン名などに対する内部名を、その表層的な名前のみならず、関数であればその引数の型や返戻値の型などといった意味的な情報を含めて修飾した名前とするものである。コンパイラからリンカ、さらには実行時のデバッガなども含んだシステム全体が、高度な型に関する情報などをサポートするように再実装するには多くの難しさがあるが、この手法であれば、システムの多くの部分ではわずかな修正で済む。特に、多重定義を許す言語では、同一の表層名に対して許される多重定義や、その同定について上手に修飾を設計すれば、扱いが単純になる。また、そのままではエラーメッセージ等が読み辛いものとなるが、「解読」ルーチンを呼ぶように修正するだけで、型の情報などが付加された、むしろわかりやすいメッセージが出力されるようになる。

1. Microsoft Windowsの場合
-----------------------

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

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

マイクロソフト  によって確立された名前修飾のスキームがあり、非公式に他のコンパイラもこれに従っている。例えば、Digital Mars・C++Builder・gccである。このスキームは他の言語、例えば、Pascal・D言語・Delphi・FORTRAN・date=2022-11にも適用される。このようにして、それら処理系のデフォルトの呼出規約が異なる場合も、それら処理系で作成したサブルーチンが現存のWindows ライブラリを呼んだり、そこから呼ばれたりすることができる。

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

int _cdecl f { return 0; } int _stdcall g { return 0; } int _fastcall h { return 0; }

_cdecl はCの標準の呼び出し規約を使うことを明示する修飾子である。

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

_f _g@4 @h@4

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

他の一般的な修飾法として、いくつかのアンダースコアで接頭辞  を付け加える、というものがある。

2. C++の場合
---------

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

C++言語は、標準的な修飾規則を定めていない。そのため、コンパイラによって修飾規則が異なる。C++の修飾がかなり複雑になりうることも加わり、異なるコンパイラのオブジェクトコードはリンクすることができないのが通常である。

2.1. 簡単な例
---------

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

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

この二つは異なった関数である。名前以外に全く関係がない。馬鹿正直にこれをCに変換すると、Cコンパイラはエラーを吐く。Cでは関数の名前の重複は許されないからである。そこで、C++コンパイラはシンボル名に型情報を加える。例えばこんな風になるだろう:

int __f_v  { return 1; } int __f_i  { return 0; } void __g_v  { int i = __f_v, j = __f_i; }

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

2.2. 複雑な例
---------

もっと複雑な例を挙げる。実際に用いられている名前修飾を見てみよう。GNU GCC 3.x系は次のクラス例をどのように修飾するだろうか。修飾されたシンボルはそれぞれの識別子の下に表示されている。

namespace wikipedia { class article { public: std::string format ; /* = _ZN9wikipedia7article6formatEv */

bool print_to ; /* = _ZN9wikipedia7article8print_toERSo */

class wikilink { public: wikilink ; /* = _ZN9wikipedia7article8wikilinkC1ERKSs */ }; }; }<WtXmlEndTag />

ここでの名前修飾スキームは比較的単純である。修飾された名前は全て _Z で始まる。アンダースコアに大文字を続けたものは、CおよびC++では処理系のために予約済みの識別子であることに注意されたい。従って、ユーザー識別子とぶつかることはない。ネストされた名前に対して、Nを付け、次いで <長さlength, 識別子id> のペアを付ける。最後にEを付ける。例えば、wikipedia::article::formatは

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

となる。

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

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

となる。

print_toの場合は、標準的な型として std::ostreamが用いられ、これには特殊な別名 So がある。よって、この型に対する参照はRSoとなる。名前の完成形は以下となる。

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

2.3. コンパイラによる名前修飾の相違
--------------------

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

注:

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

2.4. C++からリンクする際のCシンボルの扱い
-------------------------

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

ifdef __cplusplusextern "C" {

endif/* ... */

ifdef __cplusplus}

endif<WtXmlEndTag />

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

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

ifdef __cplusplusextern "C" {

endifvoid *memset ; char *strcat ; int strcmp ; char *strcpy ;

ifdef __cplusplus}

endif<WtXmlEndTag />

そこで、次のコード

if  == 0) strcpy; else memset);

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

if  == 0) __1cGstrcpy6Fpcpkc_0_; else __1cGmemset6FpviI_0_);

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

2.5. C++での名前修飾の標準化
------------------

C++で名前修飾の標準化を行うと、実装をまたいだ運用がしやすくなるというのが比較的広く信じられているが、これは実際には正しくない。名前修飾はアプリケーションバイナリインタフェース  や他の細かな言語仕様におけるいくつかの問題の一つに過ぎず、名前修飾だけをどうかしても非互換性は残ることになる。更に、特定の修飾法を決めてしまうと、実装が制限されるシステムが出現しうる。また、名前修飾を標準化してしまうと、例えばC++の文法を理解できるリンカのような、名前修飾を必要としない実装を妨げる可能性もある。

そのため、ISOではC++の標準  として、名前修飾を標準化することを特に目指してはいない。逆に、//Annotated C++ Reference Manual//では、ABI上の他の非互換性を抱えたモジュールを誤ってリンクしないように、異なった名前修飾法を用いることが推奨されている。

2.6. C++名前修飾問題の現実的な影響
---------------------

C++のシンボルはDLLや共有オブジェクトdate=2022-11}}を通してルーチン的にエクスポートされるため、名前修飾スキームはコンパイラの問題だけではすまなくなる。ライブラリをコンパイルするにあたって、複数のコンパイラによって名前修飾がそれぞれ異なったスキームで行われると、それらのライブラリを参照する際、しばしばシンボルが解決できなくなってしまう。例えば、複数のC++コンパイラが導入されているシステムにBoost C++ライブラリを導入しようとすると、二度それをコンパイルしなければならない。

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

3. Javaの場合
----------

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

3.1. 内部クラスおよび無名クラスに一意名を与える
--------------------------

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

public class Foo { // 内部クラス。 class bar { public int x; }

public void zark { // 無名クラスのインスタンス化。 Object f = new Object { public String toString { return "hello"; } }; } }<WtXmlEndTag />

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

Foo.class: 主クラス//Foo// を含む。
Foo$bar.class: //Foo.bar// という名前付きの内部クラスを含む。
Foo$1.class: メソッド //Foo.zark// に対して局所的な無名の内部クラスを含む。ドル記号  はJava仮想マシン  の仕様上許されているので、これら3つのクラス名は全て有効であり、Java言語の仕様上 $ は通常のJavaクラス定義に用いることができないので、コンパイラは安全にこれらの名前を利用することができる。

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

3.2. Java Native Interface
--------------------------

Java Native Interface  はJavaとネイティブコードを双方向に相互運用するための標準仕様である。Javaのネイティブメソッドサポートによって、Javaで記述されたプログラムから他の言語で書かれたプログラムを呼ぶことができる。ここでは2つの名前解決に関する懸念があるが、いずれも特に標準的な作法で実装されてはいない。

JVMからネイティブ名への変換: オラクルがスキームを公開している。
一般的なC++の名前修飾: 前述を参照。これとは別に、Java Native Access  は、Javaプログラムからネイティブの共有ライブラリにアクセスする方法をライブラリレベルで提供する。

4. Pythonの場合
------------

Pythonのプログラマは識別子の最初の2文字をアンダースコアにすることで、明示的にそれが「プライベートな名前」であることを示すことができる。Pythonコンパイラはこれらに遭遇すると、1個のアンダースコアとその識別子を包含するクラスの名前を先頭に追加することで、プライベートな名前を大域的なシンボルに変換する。例えばPython 2.xでは、

class Test: def __privateSymbol: pass def normalSymbol: pass

print dir<WtXmlEndTag />

は次のようになる。

['_Test__privateSymbol', '__doc__', '__module__', 'normalSymbol']

5. Turbo Pascal / Delphi の場合
----------------------------

これらのPascal処理系では、次のようにして名前修飾を抑制する。

exports myFunc name 'myFunc', myProc name 'myProc';

6. Objective-Cの場合
-----------------

Objective-Cのメソッドは、本質的に二種類に分けられる。一つは クラスメソッドで、もう一つはインスタンスメソッドである。Objective-Cでのメソッド宣言は次のような形式である。

+ //method name//: //argument// //name//1://parameter//1 ... - //method name//: //argument// //name//1://parameter//1 ...

クラスメソッドは + で示される。インスタンスメソッドは - で示される。典型的なクラスメソッド宣言は、次のようになるだろう +  initWithX:  number andY:  number; +  new; インスタンスメソッドならば、次のようである -  value; -  setValue:  new_value;

それぞれのメソッド宣言は特有の内部表現を持っている。コンパイル時に、メソッド名は次のスキームによって変換される。クラスメソッドでは _c_//Class//_//methodname//_//name//1_//name//2_ ... となり、インスタンスメソッドでは _i_//Class//_//methodname//_//name//1_//name//2_ ... となる。

Objective-Cのコロンは下線に変換される。そこで、Point クラスに属するクラスメソッド +  initWithX:  number andY:  number; は次のように変換されるだろう_c_Point_initWithX_andY_。同じクラスに属するインスタンスメソッド -  value; は _i_Point_value となる。

クラスの各メソッドはこのようにラベルされるが、全てのメソッドがこのように表現された場合、あるクラスが応答すべきメソッドを探し出すのは面倒な作業となりうる。そのため、各々のメソッドに整数のようなシンボルを一意に割り当てる。このようなシンボルは「セレクタ」として知られる。Objective-Cでは、プログラマがセレクタを直接管理することができる — Objective-Cではそれらに特別の型を与えている — SEL。

コンパイル中に、文字による表現からセレクタへのマップが作成される。文字による表現を操作するよりもセレクタを管理する方がメソッドを効果的に扱うことができる。セレクタがマッチするのはメソッドの名前だけであり、それが属するクラスではないということに注意してほしい。クラスが異なれば同じ名前のメソッドでも実装が異なることがある。このため、メソッドの実装にも特別の識別子が与えられる — 実装ポインタ  と呼ばれ、IMP 型を持つ。

オブジェクトにメッセージを送ると、それはコンパイラによって、 id objc_msgSend 関数ないしはその従兄弟のどれかに対する呼び出しとしてエンコードされる。ここで、receiverはそのメッセージの受け手であり、SELによって呼び出されるメソッドが決まる。各々のクラスはそれ自身の表を持っており、セレクタと実装 — メソッドの実体が存在するメモリ空間を指定する実装ポインタ — との相互対照ができるようになっている。また別の表にはクラスとインスタンスメソッドが記録される。SELからIMPへの対照表に格納されることはさておき、関数は本質的に無名である。

あるセレクタに対するSELの値はクラスによって変わることがなく、多態性を実現している。

Objective-Cの実行環境はメソッドの引数と返り値の型についての情報を保持しているが、メソッドの名前の一部として保持されるわけではなく、クラスによって変化しうる。

Objective-Cは名前空間をサポートしないので、クラス名を修飾する必要はない。

7. 脚注
-----

8. 関連項目
-------

呼出規約
中間表現
名前空間
言語束縛
可変長引数
コンピュータ・アーキテクチャ
オブジェクトファイル
シンボルテーブル
コールスタック
アプリケーションバイナリインタフェース 
Foreign function interface
SWIG
P/Invoke
nm 

9. 外部リンク
--------

[1]
[2] — filter to demangle encoded C++ symbols
[3]
[[:en:v:Visual C++ name mangling|Visual C++ name mangling - Wikiversity]]
[4]