Swift: swift_bridgeObjectRetain()の実装を読む
いま「Swift: 呼び出し規約を調べる」というスクラップを書いていますが、その中で出てきた関数が「swift_bridgeObjectRetain
」です。
これの説明がどこにもないので、実装を読んでみようということにしました。
まだ始めたばかりですが、思ったより相当大変なことになりそうです。
まずswift_bridgeObjectRetain()
の実装がどこにあるかというと以下です。
mainだとずれていくかも知れませんので、スナップショットタグから。
まず687-688行でいきなり訳の分からんことをしています。
if (isObjCTaggedPointer(object) || isBridgeObjectTaggedPointer(object))
return object;
引数で受け取ったオブジェクトのポインタが「ObjCのTagged Pointer」か「BridgeObjectのTagged Pointer」かを調べています。もしどちらかに該当すれば、オブジェクトのポインタをそのまま返します。この場合retain
はしないということです。
そもそもこの「Tagged Pointer」というのはなんでしょうか?
調べてみると、64ビット環境になってから「64ビットのアドレス空間をフルに使うことはないでしょ」ということで、64ビットのポインタのいくつかのビットフィールドをフラグを入れたり、なんだったらオブジェクトの指す値を入れたりしている、ということなんですね。
すべてのポインタでフラグが立てられていて、そのうち特定のフラグがONの場合は、そのポインタはもうポインタではなく、ポインタのビット列を使ってオブジェクトの値を表した特殊なビット列で、そのビット列のことをTagged Pointer(タグ付きポインタ)と呼んでいる、ということのようです。
- 64bit環境におけるObjective-Cのポインタ
(https://labs.gree.jp/blog/2015/01/13437/) - Objective-C Internals: Tagged Pointer Objects
(https://alwaysprocessing.blog/2023/03/19/objc-tagged-ptr)
ともかくisObjCTaggedPointer
とisBridgeObjectTaggedPointer
の実装を見ます。
まずisObjCTaggedPointer
から
ポインタをheap_object_abi::ObjCReservedBitsMask
でマスクして、どれかにビットが立っていたらObjCのタグ付きポインタ、と判定しています。
ObjCReservedBitsMask
の定義も見てみます。
ここで現れる_swift_abi_ObjCReservedBitsMask
はこのファイルの上のほうで定義されています。
ハードウェアやプラットフォームなどで値が変わるようですが、Apple Platformの場合、x86_64ならSWIFT_ABI_X86_64_OBJC_RESERVED_BITS_MASK
、arm64ならSWIFT_ABI_ARM64_OBJC_RESERVED_BITS_MASK
ということになりそうです。
これらの値は以下で定義されています。
同じようにisBridgeObjectTaggedPointer
も見てみます。
BridgeObjectTagBitsMask
は先ほど出てきました。= _swift_BridgeObject_TaggedPointerBits
で、これは以下で定義されています。
SWIFT_ABI_DEFAULT_BRIDGEOBJECT_TAG_64
は以下です。
arm64の場合はisObjCTaggedPointer
とisBridgeObjectTaggedPointer
で同じマスク値を使っていることになりますね。
swift_bridgeObjectRetain
の実装に戻ります。
691行目は以下です。
auto const objectRef = toPlainObject_unTagged_bridgeObject(object);
この実装は同じファイルの667行目にあります。
static void* toPlainObject_unTagged_bridgeObject(void *object) {
return (void*)(uintptr_t(object) & ~unTaggedNonNativeBridgeObjectBits);
}
unTaggedNonNativeBridgeObjectBits
の定義も同じファイルの562行目にあります。
static auto const unTaggedNonNativeBridgeObjectBits
= heap_object_abi::SwiftSpareBitsMask
& ~heap_object_abi::ObjCReservedBitsMask
& ~heap_object_abi::BridgeObjectTagBitsMask;
これも一回見ました。SwiftSpareBitsMask
は、Apple Platformの場合、x86_64で
arm64の場合、
です。
実装のコメントに書いてあります。x86_64だと、最上位1バイトは使わなくて、かつ最小でも8バイトアラインなので下位3ビットも使わない、なので0xFF00000000000007ULL
、ということですね。
arm64だと、本当は(x86_64同様)最上位1バイトは使わないんだけどARMv8.5-Aはその下位4ビットをメモリタギングに使う、ということですね。またこちらも最小8バイトアラインなので、下位3ビットも使わない、なので0xF000000000000007ULL
、ということですね。
これらをまとめるとunTaggedNonNativeBridgeObjectBits
は以下のようになります。
- x86_64:
0xFF00000000000007ULL
- arm64:
0xF000000000000007ULL
toPlainObject_unTagged_bridgeObject()
は、引数で渡されたポインタの値に対してこれらのビットを落として返す、ということになります。これらのビットが立った状態でポインタのアドレスにアクセスすると、アライメントエラーだったりメモリ領域外エラーだったりが発生してしまいます。
続いて以下の部分を見ていきます。
objectRef
は作ったんだけど、もう1回もとのポインタであるobject
をチェックします。
このisNonNative_unTagged_bridgeObject
は同じファイルに定義があります。
コメントを見ると「与えられたBridgeObjectのポインタが、ネイティブ参照カウンタを使うものと知られていない場合trueを返す」とあります。この場合のネイティブ参照カウンタは、関数名を考えるとSwiftの参照カウンタ、ということでしょう。Swiftの参照カウンタを使わない場合、trueを返す。ということですね。
Swiftの参照カウンタを使わない=Objective-Cの参照カウンタを使う、です。
ポインタのビットのうちのobjectPointerIsObjCBit
というのを見てそれをチェックしています。
ということで、このビットが立っていればObjective-Cのオブジェクトのポインタ、そうでなければSwiftのオブジェクトのポインタと見なします。
Swiftオブジェクトなら、フラグ類を落としたポインタ(=objectRef
)に対してswift_retain()
を呼びます。
Objective-Cオブジェクトならフラグ類を落とす前のポインタ(=object
)に対してobjcRetainAndReturn()
を呼びます。
objcRetainAndReturn()
の定義を見てみます。
結局もう1回、toPlainObject_unTagged_bridgeObject()
をしてそれをobjc_retain()
しているだけです。なんだそりゃ。
なんだそりゃ。
の答えは、コメントに書いてありました。
// Put the call to objc_retain in a separate function, tail-called here. This
// allows the fast path of swift_bridgeObjectRetain to avoid creating a stack
// frame on ARM64. We can't directly tail-call objc_retain, because
// swift_bridgeObjectRetain returns the pointer with objectPointerIsObjCBit
// set, so we have to make a non-tail call and then return the value with the
// bit set
-
swift_brigeObjectRetain
は、objectPointerIsObjCBit
が立った状態のポインタを返す必要がある - non-tail callをして、このビットが立ったポインタを返す必要がある。
tail-callというのはなんでしょうか。ARM64にだけ存在する機能ですかね。
tail-callをするとスタックフレームを作らなくてよくなり、swift_bridgeObjectRetain
が速くなる、というように読めます。
詳しくはまだよく分かっていませんが、以下の記事で説明されているようなことなんですかね。ARMではret
が省略されるという。
tail-callは、関数の最後にある別の関数の呼び出しをcall
でなくjump
に置き換えて、スタックポインタの操作やローカル変数の確保を省略する、というような最適化テクのようです。
ARMはこういうことがしやすいということですかね。