Share to: share facebook share twitter share wa share telegram print page

 

名前修飾

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

Microsoft Windowsの場合

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

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

マイクロソフト (MS) によって確立された名前修飾のスキームがあり、非公式に他のコンパイラもこれに従っている。例えば、Digital MarsC++BuilderボーランドC)・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の標準の呼び出し規約を使うことを明示する修飾子である。

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

_f
_g@4
@h@4

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

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

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では関数の名前の重複は許されないからである。そこで、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++では処理系のために予約済みの識別子であることに注意されたい[1][2]。従って、ユーザー識別子とぶつかることはない(ものとみなせる)。ネストされた名前(名前空間とクラスの両方)に対して、Nを付け、次いで <長さlength, 識別子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)
clang 1.x _Z1hi _Z1hic _Z1hv
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 VAXDEC AlphaIA-64を除く)および Tru64 UNIX 上のCompaq C++ コンパイラは2種類の修飾スキームを持っている。標準化前のもともとのスキームは ARM モデルとして知られていた。これは『The Annotated C++ Reference Manual (ARM)』(邦訳『注解C++リファレンスマニュアル』)に記述された方法を元にしている。標準C++の機能拡充、特にテンプレート機能の追加に伴い、ARM は次第に旧式化していった — ある種の関数型をエンコードできず、異なった関数に異なったシンボル名を割り当てることができなくなっていた。そこで、より新しい ANSI モデルが導入され、ANSIテンプレート機能が全て利用可能になったが、過去の版との互換性は失われた。todo: the different isn't obvious from the examples. maybe a template or something should be added...
  • IA-64にはアプリケーションバイナリインタフェース (ABI) 標準の名前修飾規則(Itanium C++ ABI mangling)が存在する(#外部リンク参照)。これは標準的な名前修飾スキームを定義したものであり、全ての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++で名前修飾の標準化を行うと、実装をまたいだ運用がしやすくなるというのが比較的広く信じられているが、これは実際には正しくない。名前修飾はアプリケーションバイナリインタフェース (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 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 ファイルが生成される。

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

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

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

Java Native Interface

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

  • JVMからネイティブ名への変換: オラクルがスキームを公開している[3]
  • 一般的なC++の名前修飾: 前述を参照。

これとは別に、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は名前空間をサポートしないので、クラス名を修飾する必要はない。

脚注

関連項目

外部リンク

Kembali kehalaman sebelumnya