Objective-C では、オブジェクトに対するほとんど全ての操作がメソッド呼び出しの形で記述されます。したがって、メソッド呼び出しの方法さえ分かれば Objective-C のランタイムの操作は大抵できてしまいます。この章ではまず Objective-C でどのように記述するかをおさらいした後、C のランタイム API を使って書き直す方法を紹介します。

Objective-C のメソッド呼び出し

Objective-C のメソッド呼び出しの記法は、C に対する言語の拡張として実現されています。Objective-C のコードを書いたことがある人ならすでに十分に親しんでいると思いますが、後に C の API を使った表現と比べるため、ここで簡単におさらいしておきましょう。

オブジェクトに対してメソッドを呼び出すには、例えば次のように書きます:

NSString *str = @"Hello";
NSString *newStr = [str substringFromIndex: 1];

この例ではまず str という名前の NSString オブジェクトを作った上で、これに対して substringFromIndex: というメソッドを呼び出しています。この場合は引数は 1 個で 1 という値を渡しています。メソッドの戻り値は newStr という変数に格納されます。

引数が 2 個の場合は次のような形式で呼び出します:

NSString *replaced = [str stringByReplacingOccurrencesOfString: @"e"
                                                    withString: @"a"];

API のリファレンスマニュアルでは、このメソッドは stringByReplacingOccurrencesOfString:withString: という項目として掲載されています。このように途中に : を含むような形式がメソッドの名前となります。また、逆にこのメソッド名には 2 個の : が含まれることから、メソッド名を見ただけで引数を 2 個とるメソッドであることがわかります。

objc_msgSend

次にこれを同様のメソッド呼び出しを Objective-C ランタイム API を使って実現しましょう。これは C の API なので、通常の C の関数呼び出しの形をしています。オブジェクトに対してメソッドを呼び出すには objc_msgSend という関数を使います。上の最初の例でとりあげたメソッドは次のように書けます:

#import <objc/runtime.h>

NSString *newStr = objc_msgSend(str, @selector(substringFromIndex:), 1);

第 1 引数は対象となるオブジェクトです。その次の引数は特殊な形(コンパイラディレクティブ)をしていますが、これはメソッドのセレクタを取得するための構文です。セレクタについては後で詳しく見ます。最後の引数はメソッドの引数です。

この関数をもう少し良く見てみましょう。まず頭の objc_ は、Objective-C ランタイムの API 関数のうちで、オブジェクトやクラスなどの特定のデータ型に関連しないものにつきます。といってもちょっと分かりにくいですが、後に出てくる他の API 関数と比べてみると位置付けが分かります。

次の msgSend の部分は、メッセージmsg)を送る(Send)という操作を表します。これは Objective-C がメソッド呼び出しのことをメッセージ送信として扱っていることを反映しています。特定のオブジェクト(レシーバ)に対してメッセージを送ることで色々な機能を実現するという考え方で、Smalltalk というプログラミング言語に強く影響を受けています。

もしかしたら MSG という文字を見ると「味の素」のような化学調味料(グルタミン酸ナトリウム)を想像する人がいるかもしれませんが、もちろん別物です。しかしメッセージというのはオブジェクト間でやり取りされるある種の神経伝達物質のようなものと考えることもでき、味の素のように少量で強烈な旨味を人間に感じさせる物質を連想するのは、あながち間違ってはいないかもしれません。

セレクタ

メソッドのセレクタとは、それぞれのメソッド名につけられたユニークな ID のようなものです。上のコードでは @selector という特殊な形式の構文を使っていますが、次のように純粋に C の関数呼び出しだけを用いて記述することもできます。

SEL sel = sel_registerName("substringFromIndex:");
NSString *newStr = objc_msgSend(str, sel, 1);

セレクタは SEL という型の値として扱います。心配性な人は SEL 型の値を変数に代入したときに実際に何が起こるか心配になるかもしれません。もしこれが構造体なら、変数への代入時にメンバの値がコピーされることになり、そのために余計なオーバーヘッドや不整合が起こり得るのではないかと心配になるでしょう。

これは、Objective-C ランタイムのリファレンスによると次のように宣言されています:

typedef struct objc_selector *SEL;

つまり、何かの構造体のポインタです。したがって値のコピーによるオーバーヘッドを気にせず、安心して変数に代入したり関数の引数として渡すことができます。

では、これがポインタなのは分かったとして、次にポインタはいつまで有効なのかが心配になるかもしれません。結論から言うと、その心配は無用です。

sel_registerName という関数は次のようにプロトタイプ宣言されています:

SEL sel_registerName(const char *str);

この関数は sel_ で始まっていることから分かるように、セレクタに関する操作です。そして、registerName という名前が示すように、セレクタの名前を登録するという操作がおこなわれます。ある名前に対応したセレクタを取り出したいだけなのに、なぜ登録をする関数を呼び出すかというと、この関数はすでに名前が登録済みであれば、何もせずにその登録されたセレクタを返すからです。存在しないメソッド名を引数に渡すと、ランタイムは新しくセレクタを登録した上でそれを返します。

また、Objective-C ランタイム API を見る限り、セレクタを削除するという機能は提供されていません。

したがって、一度取得したセレクタは、SEL という型の不透明な値として、その後いつまでも有効であると考えられます。

メッセージ

上述したように Objective-C のメソッド呼び出しは、メッセージという形をとって表現されています。これは Smalltalk から強い影響を受けた結果です。メッセージはセレクタと引数からなり、オブジェクトがメッセージを受け取るとそのセレクタに対応した関数が選ばれて呼び出されます。ここでセレクタとそれに対応する関数の組み合わせをメソッドと呼びます。

これは Smalltalk に親しみがないといまいちピンとこないかもしれません。その場合は Pharo などの Smalltalk 処理系をインストールして簡単なチュートリアルをやってみるといいと思います。Smalltalk に触れる事で Objective-C の不可解に思える部分にも親しみを感じられるようになるかもしれません。

ただし、Objective-C ランタイム API を使ったプログラミングでは、あまりメッセージというものを意識する必要はありません。最終的にはただの C の関数呼び出しに還元された上で実行されます。