Objective-C の季節 - メタクラス、インスタンス変数リスト

早春といったら Objective-C の季節ではないでしょうか?いえ、そんなことは別になくて、仕事で Objective-C 使うことになったので、仕方なく調べてみようという話です。そんなわけで Objective-C を知ろうのコーナー、始まります。ubuntu で試しているので何か Mac だと違うよーとかがあるかも、ないかも…


Objective-C は、ボクの中では C にでっかいランタイムと少しの(奇妙な)文法が加わった、所謂動的型付けのオブジェクト指向な言語、という認識なんですが、これはボクの認識なのでどうでもいいです。
兎に角、動的言語ですから、何か物を調べるなら、実行時に調べるのが一番だということです。

こんなコードをコンパイルして、

#import <Foundation/NSObject.h>
int main() {
  NSObject* obj = [[NSObject alloc] init];
  return 0;
}

デバッガで色々調べて見てみます。

(gdb) p obj
$1 = (struct NSObject *) 0x805d0e0
(gdb) ptype NSObject
type = struct NSObject {
protected:
struct objc_class *isa;
}

NSObject クラスは、実際には objc_class 構造体へのポインタ型の isa メンバ変数を一つ持つ、NSObject 構造体として定義されているみたいですね。
isa というからには is-a つまり、そのオブジェクトのクラスを表すんでしょう。

(gdb) p *obj->isa
$3 = {class_pointer = 0x459d80, super_class = 0x0, name = 0x458df8 "NSObject", version = 0, info = 11534349, instance_size = 4,
ivars = 0x459a60, methods = 0x433460, dtable = 0x8076490, subclass_list = 0x46a5a0, sibling_class = 0x0, protocols = 0x459d70,
gc_object_type = 0x0}
(gdb) ptype struct objc_class
type = struct objc_class {
MetaClass class_pointer;
struct objc_class *super_class;
const char *name;
long int version;
long unsigned int info;
long int instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list *methods;
struct sarray *dtable;
struct objc_class *subclass_list;
struct objc_class *sibling_class;
struct objc_protocol_list *protocols;
void *gc_object_type;
}

メタクラススーパークラスインスタンス変数リスト、メソッドリスト、それっぽいものをいっぱい持ってますね。まあ一つずつ見ていきましょう。とりあえずメタクラスから。ところでメタクラスが isa じゃなくて class_pointer なのは少し興味深いですよね。レイアウトは一緒なので、キャストして isa としてみたりもするのかも知れませんが。

(gdb) p *obj->isa->class_pointer
$5 = {class_pointer = 0x148d80, super_class = 0x148dc0, name = 0x458df8 "NSObject", version = 0, info = 11534350, instance_size = 52,
ivars = 0x4599c0, methods = 0x806cc38, dtable = 0x8074508, subclass_list = 0x46a560, sibling_class = 0x44cb20, protocols = 0x459d70,
gc_object_type = 0x0}

ヤヤッ、NSObject のメタクラスは NSObject でした…!しかし、よくよく見ると、最初に見た NSObject はスーパークラスを持っていませんでしたが、こちらは持っています。同名の、実体の異なるクラスみたいですね。名前が同じだとややこしいですから、こっちはメタ NSObject クラスと呼びましょう*1。その他の詳細は後で調べるとして、メタ NSObject クラスのメタクラスを見てみます。

(gdb) p *obj->isa->class_pointer->class_pointer
$7 = {class_pointer = 0x148d80, super_class = 0x148dc0, name = 0x1489fb "Object", version = 0, info = 131082, instance_size = 52,
ivars = 0x148a40, methods = 0x806c808, dtable = 0x804b430, subclass_list = 0x149120, sibling_class = 0x459660, protocols = 0x0,
gc_object_type = 0x0}

メタ NSObject のメタクラス は Object みたいです。じゃあそのメタクラスは?

(gdb) p *obj->isa->class_pointer->class_pointer->class_pointer
$49 = {class_pointer = 0x148d80, super_class = 0x148dc0, name = 0x1489fb "Object", version = 0, info = 131082, instance_size = 52,
ivars = 0x148a40, methods = 0x806c808, dtable = 0x804b430, subclass_list = 0x149120, sibling_class = 0x459660, protocols = 0x0,
gc_object_type = 0x0}
(gdb) p *obj->isa->class_pointer->class_pointer->class_pointer->class_pointer
$50 = {class_pointer = 0x148d80, super_class = 0x148dc0, name = 0x1489fb "Object", version = 0, info = 131082, instance_size = 52,
ivars = 0x148a40, methods = 0x806c808, dtable = 0x804b430, subclass_list = 0x149120, sibling_class = 0x459660, protocols = 0x0,
gc_object_type = 0x0}

循環している…!数字が急に飛んでることからボクが何したかは大体分かってもらえると思います。遊んでしまいました。
兎に角、Object のメタクラスは Object 自身、メタメタクラスも Object 自身、メタメタメタクラスも Object 自身、メタメタメタメタクラスも Object 自身です。
それじゃあ、スーパークラスは?というと、

(gdb) p *obj->isa->class_pointer->class_pointer->super_class
$51 = {class_pointer = 0x148d80, super_class = 0x0, name = 0x1489fb "Object", version = 0, info = 131081, instance_size = 4,
ivars = 0x148ae0, methods = 0x4422fc, dtable = 0x8073f68, subclass_list = 0x49cb20, sibling_class = 0x0, protocols = 0x0,
gc_object_type = 0x0}

矢張り Object でした!しかし、先の Object と違い、スーパークラスを持たないようです。区別するために、このクラスはルート Object クラスと呼びましょう*2

さて次は、それぞれのオブジェクトが、どのようなインスタンス変数を持っているのかを調べてみましょう。
まず重要なファクターとして、クラスがそのクラスのオブジェクトのインスタンス変数に関する情報を持っている、というのがありますね。例えば Ruby の場合、オブジェクトがどのようなインスタンス変数を持っているか、という情報は、オブジェクト自身にしかない。インスタンス変数を表すハッシュテーブルに実際に問い合わせて始めて、インスタンス変数が存在するかどうかが分かる。これは柔軟さと、それに伴う危うさがあるわけですが、その点で比べてみると、Objective-C はクラスごとにインスタンス変数に関しての規約がある、ということで、Ruby に比べると若干「硬い」といえると思います。
Objective-C では、既存のクラスを入れ替えるような操作が限定的に許されていたと思うのですが*3インスタンス変数は増やしたり減らしたりできなかったはずで*4、その辺からも Objective-C が、クラスとオブジェクトのインスタンス変数の関係を、それなりに重要視しているというのが分かりますね。

閑話休題インスタンス変数のリストを表す objc_ivar_list 構造体のレイアウトを見てみます。

(gdb) ptype struct objc_ivar_list
type = struct objc_ivar_list {
int ivar_count;
struct objc_ivar ivar_list[1];
}

ivar_count はインスタンス変数の個数、ivar_list はインスタンス変数のリスト、見たままです。ivar_list は所謂 flexible array member 的なもののようですね。言語仕様の定義的には違ったと思いますが、同じ使い方してるのでまあそういうことで…

次は objc_ivar です。

(gdb) ptype struct objc_ivar
type = struct objc_ivar {
const char *ivar_name;
const char *ivar_type;
int ivar_offset;
}

インスタンス変数の名前と、型情報と、値の位置を指し示すオフセットで構成されています。まあこれも見たままですね。
ということで実際の値を調べてみます。とりあえず、明らかにインスタンス変数の少なそうな、ルート Object クラスから。

(gdb) p *obj->isa->class_pointer->class_pointer->super_class->ivars
$52 = {ivar_count = 1, ivar_list = {{ivar_name = 0x148480 "isa", ivar_type = 0x1485f8 "#", ivar_offset = 0}}}

型のエンコードに関してはこのあたりを参照して下さい。# はクラス、ということなので、つまるところ、ルート Object クラスは isa というインスタンス変数を一つ持っていて、中身はクラス、オフセットは 0 だよ、ということを言っているらしい。オフセット 0 というのはつまり、class_pointer メンバ変数のことですから、まあ矢張り isa として使うんだなーという感じですね。

次は、ルートじゃない Object クラスを見てみます。

(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[0]
$62 = {ivar_name = 0x148480 "isa",
ivar_type = 0x1484a0 "^{_objc_class=^{_objc_class}^{_objc_class}*lll^{_objc_ivar_list}^{_objc_method_list}^{sarray}^{_objc_class}^{_objc_class}^^{_objc_protocol}^v}", ivar_offset = 0}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[1]
$63 = {ivar_name = 0x14852f "super_class",
ivar_type = 0x1484a0 "^{_objc_class=^{_objc_class}^{_objc_class}*lll^{_objc_ivar_list}^{_objc_method_list}^{sarray}^{_objc_class}^{_objc_class}^^{_objc_protocol}^v}", ivar_offset = 4}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[2]
$64 = {ivar_name = 0x14853b "name", ivar_type = 0x148540 "*", ivar_offset = 8}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[3]
$65 = {ivar_name = 0x148542 "version", ivar_type = 0x14854a "l", ivar_offset = 12}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[4]
$66 = {ivar_name = 0x14854c "info", ivar_type = 0x14854a "l", ivar_offset = 16}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[5]
$67 = {ivar_name = 0x148551 "instance_size", ivar_type = 0x14854a "l", ivar_offset = 20}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[6]
$68 = {ivar_name = 0x14855f "ivars", ivar_type = 0x148565 "^{_objc_ivar_list=}", ivar_offset = 24}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[7]
$69 = {ivar_name = 0x148579 "methods", ivar_type = 0x148581 "^{_objc_method_list=}", ivar_offset = 28}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[8]
$70 = {ivar_name = 0x148597 "dtable", ivar_type = 0x14859e "^{sarray=}", ivar_offset = 32}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[9]
$71 = {ivar_name = 0x1485a9 "subclass_list",
ivar_type = 0x1484a0 "^{_objc_class=^{_objc_class}^{_objc_class}*lll^{_objc_ivar_list}^{_objc_method_list}^{sarray}^{_objc_class}^{_objc_class}^^{_objc_protocol}^v}", ivar_offset = 36}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[10]
$72 = {ivar_name = 0x1485b7 "sibling_class",
ivar_type = 0x1484a0 "^{_objc_class=^{_objc_class}^{_objc_class}*lll^{_objc_ivar_list}^{_objc_method_list}^{sarray}^{_objc_class}^{_objc_class}^^{_objc_protocol}^v}", ivar_offset = 40}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[11]
$73 = {ivar_name = 0x1485c5 "protocol_list", ivar_type = 0x1485d3 "^^{_objc_protocol}", ivar_offset = 44}
(gdb) p obj->isa->class_pointer->class_pointer->ivars->ivar_list[12]
$74 = {ivar_name = 0x1485e6 "gc_object_type", ivar_type = 0x1485f5 "^v", ivar_offset = 48}

単に構造体メンバと一対一で対応しているだけで、特にこれ、というものはないですね。まあ構造体ありきなので当然なのですが。しかし、型情報が完全に読めなくて面白いです…

書きかけなのに保存してしまった。まあいいか。

*1:勝手に名づけました

*2:勝手に名づけました

*3:うそかも

*4:うそかも