A Bridge Too Far

iPhoneのおかげでObjective-Cがかなりメジャーな言語となった。それはうれしいことだ。わたしはObjective-Cが好きで、それは元を正せばSmalltalkが好きだというところに通じる。ただObjective-Cはメリットの裏表で実行時のパフォーマンスに難点がある。いや、それさえも回避可能ではあるけど、記述の利便性が損なわれたり、よく知ってないとできないことも多い。

例えばメッセージ式で型推定は行われないので、毎回素直にセレクタに対応するメソッドを探してから呼び出す。もちろんキャッシュの仕組みなんかはあるのだけど、直接コールするよりは何倍も重い処理になっている。以前3Dエンジンを実装したとき、ステート管理を素直にメッセージ式で呼び出すいわゆるObjective-Cのクラスで実装したら劇的に遅くなって愕然としたことがある。メッセージ式は大量に呼び出す必要があるとき、とくに回数の多いループの中なんかは要注意だ。

Objective-C 2.0で実現されたプロパティも困った問題を引き起こすことがある。アクセス方法がC/C++でなじみのドットアクセスなので、ついついその感覚で使ってしまうのだろうけど、パフォーマンスに影響あるところで

@property (nonatomic) Vector *position;
...
object.position.x = 100;
object.position.y = 100;
object.position.z = 0;

というような記述を見てくらくらとしたことがある。これは、つまり

[[object position] setX:100];
[[object position] setY:100];
[[object position] setZ:0];

だと考えれば、どれだけのコストがかかってるか分かるのだけど、簡便な記述がそれを見えにくくしている。

ただ、Objective-Cを使う人口が増えるということは、コンパイラに手を入れる人間の数も増えるということなので、近い将来にパフォーマンスのかなりの部分をコンパイラが面倒を見てくれるようになるかもしれない。

さて、今回も枕が長くなったが、Objective-Cの(実装内容を見れば実は普通のCの機能なのだけど)Toll-Free-Bridgeという仕組みを調べて行くうちに、いろいろとパズル的にC++側から見れば純粋なC++なクラス、Objective-C側から見れば純粋なObjective-Cの実装というのができないかと考えてみたというのが本題である。

図解するとこんな感じだ。

Objective-CにはObjective-C++という仕組みがあって、実装ソースの拡張子を.mmにすればC++の機能も同時に使えるようになるので、なぜこんな面倒なことをと思う方もいるかもしれない。一つには、特にC++側は純粋なC++コードであることが多く、組み合わせる必要がある度にそれらのソースを.mmにするわけにはいかないことと、魔法のように.mmにすれば解決というのが嫌だったのだ。

ネット上で有力な日本語のObjective-Cの情報ソースの一つといえばマイコミジャーナルで連載していたダイナミックObjective-Cで異論はないだろう。Cocoa等のフレームワークの情報だけでなく、言語そのものを扱っている情報ソースは意外と少ない。

Objective-Cのクラスはobjc/objc.hおよびobjc/runtime.hにおいて定義されているとおり

typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
struct objc_class {
Class isa;
...

先頭にisaというポインタがあるただの構造体だ。こういう実装の屋台骨が見えるのもObjective-Cの面白いところだと思う。

C++/Objective-Cの透過的連携に関してはこちらにとても興味深い記事があったのだけど、残念ながら2つの理由によりこの方法はとることができない。一つはpure C++環境では@defsが通らないこと。そしてもう一つが、この方法では仮想関数のあるC++クラスが作れないことだ。

なぜ仮想関数のあるC++クラスがダメかというと

class Test
{
private:
int m_counter;
public:
Test() {}
~Test() {}
void test() const { printf("%p=%p\n", this,&m_counter); }
}
class Test2
{
private:
int m_counter;
public:
Test2() {}
virtual ~Test2() {}
void test() const { printf("%p=%p\n", this,&m_counter); }
}
...
Test *test = new Test;
Test2 *test2 = new Test2;
test->test();
test2->test();

この、デストラクタにvirtualをつけたかどうかしか違わない2つのクラスのtest()の実行結果は異なる。つまり、virtual無しの場合、thisが指すアドレスと先頭の要素のアドレスが同じで、そうでない場合は異なるということだ。もちろん、これはvirtualがついてある方の先頭に仮想関数テーブルへのポインタが存在するためだ。

Objective-Cのクラスは前述の通り先頭にisaへのポインタのある構造体なので、クラスでも先頭にisaを置いてやればキャストですむのだけど、仮想関数を持つクラスは先頭に(暗黙で)仮想関数テーブルへのポインタが存在してしまうため、キャストすることができない。

仮想関数がある場合はダメなら使わなければ良いというのは、そうもいかない。なぜなら仮想関数はクラスの派生に関わってくるためだ。デストラクタをvirtualにできないと派生クラスの実装が不自由になるのだ。すくなくとも、わたしはそうだ。

では、単純に仮想関数テーブル分ポインタを足してからキャストすれば良いというのもうまくいかない。仮想関数テーブルは仮想関数のあるクラスにしか存在しないし、それは実行時型情報からも調べることができない。newしてみてアドレスを比較してみるという手も考えられなくはないけども、あまりにもあまりな感じがする。

あれはできてこれはできない、そういう制約条件がまるでパズルのようだ。

さて、ポインタをずらす必要があるかどうか場合によって違うのなら、メモリ配置を工夫していつも決まった値だけずらせば他方が得られるという実装を考える。具体的にはisaを仮想関数テーブルより前に置くことを考える。

Foundation FrameworkにNSAllocateObjectという関数があり、この第2引数にextraBytesつまり、あるクラスのインスタンスを作成するときに余分に指定バイト大きく作ることができる機能がある。この部分にC++クラスのインスタンスを押し込もうというわけだ。Objective-Cのクラス要素が空の場合、先頭のisaをスキップすればC++のインスタンスへのポインタになる。

C++の_BridgeBaseクラスとObjective-CのBridgeBaseクラスの実装を考える。

C++のクラスに、まずメモリの確保・解放のためのクラスメソッドを実装する。

static void *alloc(size_t in_size,const char *in_objcClassName);
static void dealloc(void *);
...
void *
_BridgeBase::alloc(size_t in_size,const char *in_objcClassName)
{
id klass = objc_getClass(in_objcClassName);
unsigned char *p = (unsigned char *)NSAllocateObject(klass, in_size, NULL);
return p + sizeof(void *);
}
void
_BridgeBase::dealloc(void *in_space)
{
objc_class *obj = (objc_class *)((unsigned char *)in_space - sizeof(void *));
NSDeallocateObject(obj);
}

指定されたObjective-Cのインスタンスを作成し、C++のインスタンス用のアドレス分ずらして返す、逆にC++インスタンスのアドレスからもとのObjective-Cのインスタンスのアドレスに変換した後解放する処理だ。

そして、これに対応するようにnew/deleteをoverride/overloadする。

static void *operator new(size_t in_size,const char *in_objcClassName) { return alloc(in_size,in_objcClassName); }
static void operator delete(void *in_memory) { dealloc(in_memory); }

わざわざnew/deleteの実装を別に分けてるのは派生時に便利なためだ。

ここまでの実装でC++側からはインスタンスを作成できるようになった。

_BridgeBase *base = new("BridgeBase") _BridgeBase();

new時にObjective-C側のクラス名を指定する必要があるのが面倒だけど、とりあえず置いておいて実装を進めよう。

Objective-C側も実装する前に、キャスト用のテンプレート関数も実装しておく。

template
T objc2cpp_cast(id obj) { return (T)((unsigned char *)obj + sizeof(void *)); }
template
id cpp2objc_cast(T obj) { return (id)((unsigned char *)obj - sizeof(void *)); }

Objective-C側で確保したメモリよりC++側のインスタンスを作成(初期化)するために、さらにnewをoverloadしておく。

static void *operator new(size_t ,void *in_memory) { return in_memory; }

渡されたアドレスをそのまま返すという単純な実装。

そして、Objective-C側の実装。

@implementation BridgeBase
+ (id)alloc
{
id obj = NSAllocateObject(self, sizeof(_BridgeBase), NULL);
new(objc2cpp_cast<_BridgeBase *>(obj)) _BridgeBase();
return obj;
}
+ (id)allocWithZone:(NSZone *)zone
{
id obj = NSAllocateObject(self, sizeof(_BridgeBase), zone);
new(objc2cpp_cast<_BridgeBase *>(obj)) _BridgeBase();
return obj;
}
- (void)dealloc
{
delete objc2cpp_cast<_BridgeBase *>(self);
}

deallocの実装が実質deleteになっているのは、省力化のため。allocがそうなってないのは手を抜いたためである。

さらに、C++の派生クラスの実装に便利なようにマクロを定義しておく。

#define BRIDGE_TO( A )	\
static void *operator new(size_t ,void *in_memory) { return in_memory; }	\
static void *operator new(size_t in_size) { return alloc(in_size,#A); }		\
static void operator delete(void *in_memory) { dealloc(in_memory); }

これを上で実装したnew/deleteの代わりに使うことによって、

class _BridgeBase
{
public:
BRIDGE_TO( BridgeBase )

と簡易な記述が可能になる。また、new時も

_BridgeBase *base = new _BridgeBase;

と見慣れた形式で使用が可能になる。

テスト用にそれぞれ、

virtual void setCount(int in_count) { m_count = in_count; }
int count() const { return m_count; }
private:
int m_count;
@property (nonatomic) int count;
...
- (void)setCount:(int )count
{
objc2cpp_cast<_BridgeBase *>(self)->setCount(count);
}
- (int)count
{
return objc2cpp_cast<_BridgeBase *>(self)->count();
}

を追加して、

_BridgeBase *base = new _BridgeBase;
base->setCount(1);
NSLog(@"%d/%d",[cpp2objc_cast<>(base) count],[cpp2objc_cast<>(base) retainCount]);
[cpp2objc_cast<>(base) release];
BridgeBase *bridge = [[BridgeBase alloc] init];
bridge.count = 2;
NSLog(@"%d",objc2cpp_cast<_BridgeBase *>(bridge)->count());
delete objc2cpp_cast<_BridgeBase *>(bridge);

を実行してみると期待通りの実行結果になる。

もちろん、派生クラスに関しても動作する。

class _BridgeDerived : public _BridgeBase
{
public:
BRIDGE_TO( BridgeDerived )

のように、必ずBRIDGE_TO()は記述する必要がある。

C++側は_BridgeBaseクラスから派生しておまじないを書けば簡単に実装できるが、Objective-C側はかなり煩雑な記述が必要である。また、派生したC++のクラスの実装は通常のcppで実装が可能だが、Objective-C側は必ずmmである必要がある。new []、delete[]の実装もサボっているので、必要ならば実装しないといけない。

まとめたソースはgistに置いてある。