Open9

Swift: swift_bridgeObjectRetain()の実装を読む

kabeyakabeya

いま「Swift: 呼び出し規約を調べる」というスクラップを書いていますが、その中で出てきた関数が「swift_bridgeObjectRetain」です。

これの説明がどこにもないので、実装を読んでみようということにしました。
まだ始めたばかりですが、思ったより相当大変なことになりそうです。

kabeyakabeya

まずswift_bridgeObjectRetain()の実装がどこにあるかというと以下です。
mainだとずれていくかも知れませんので、スナップショットタグから。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/runtime/SwiftObject.mm#L685-L711

まず687-688行でいきなり訳の分からんことをしています。

if (isObjCTaggedPointer(object) || isBridgeObjectTaggedPointer(object))
    return object;

引数で受け取ったオブジェクトのポインタが「ObjCのTagged Pointer」か「BridgeObjectのTagged Pointer」かを調べています。もしどちらかに該当すれば、オブジェクトのポインタをそのまま返します。この場合retainはしないということです。

そもそもこの「Tagged Pointer」というのはなんでしょうか?

kabeyakabeya

調べてみると、64ビット環境になってから「64ビットのアドレス空間をフルに使うことはないでしょ」ということで、64ビットのポインタのいくつかのビットフィールドをフラグを入れたり、なんだったらオブジェクトの指す値を入れたりしている、ということなんですね。
すべてのポインタでフラグが立てられていて、そのうち特定のフラグがONの場合は、そのポインタはもうポインタではなく、ポインタのビット列を使ってオブジェクトの値を表した特殊なビット列で、そのビット列のことをTagged Pointer(タグ付きポインタ)と呼んでいる、ということのようです。

ともかくisObjCTaggedPointerisBridgeObjectTaggedPointerの実装を見ます。
まずisObjCTaggedPointerから

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/runtime/Private.h#L157-L164

ポインタをheap_object_abi::ObjCReservedBitsMaskでマスクして、どれかにビットが立っていたらObjCのタグ付きポインタ、と判定しています。
ObjCReservedBitsMaskの定義も見てみます。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/HeapObject.h#L256-L267

ここで現れる_swift_abi_ObjCReservedBitsMaskはこのファイルの上のほうで定義されています。
ハードウェアやプラットフォームなどで値が変わるようですが、Apple Platformの場合、x86_64ならSWIFT_ABI_X86_64_OBJC_RESERVED_BITS_MASK、arm64ならSWIFT_ABI_ARM64_OBJC_RESERVED_BITS_MASKということになりそうです。

これらの値は以下で定義されています。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/System.h#L127

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/System.h#L170

同じようにisBridgeObjectTaggedPointerも見てみます。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/runtime/SwiftObject.mm#L657-L659

BridgeObjectTagBitsMaskは先ほど出てきました。= _swift_BridgeObject_TaggedPointerBitsで、これは以下で定義されています。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/HeapObject.h#L145-L146

SWIFT_ABI_DEFAULT_BRIDGEOBJECT_TAG_64は以下です。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/System.h#L67

arm64の場合はisObjCTaggedPointerisBridgeObjectTaggedPointerで同じマスク値を使っていることになりますね。

kabeyakabeya

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で

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/HeapObject.h#L131-L132

arm64の場合、

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/HeapObject.h#L131-L132

です。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/System.h#L119-L123

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/System.h#L69-L70

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/SwiftShims/swift/shims/System.h#L163-L166

実装のコメントに書いてあります。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()は、引数で渡されたポインタの値に対してこれらのビットを落として返す、ということになります。これらのビットが立った状態でポインタのアドレスにアクセスすると、アライメントエラーだったりメモリ領域外エラーだったりが発生してしまいます。

kabeyakabeya

続いて以下の部分を見ていきます。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/runtime/SwiftObject.mm#L694-L696

objectRefは作ったんだけど、もう1回もとのポインタであるobjectをチェックします。
このisNonNative_unTagged_bridgeObjectは同じファイルに定義があります。

https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/runtime/SwiftObject.mm#L644-L654

コメントを見ると「与えられたBridgeObjectのポインタが、ネイティブ参照カウンタを使うものと知られていない場合trueを返す」とあります。この場合のネイティブ参照カウンタは、関数名を考えるとSwiftの参照カウンタ、ということでしょう。Swiftの参照カウンタを使わない場合、trueを返す。ということですね。
Swiftの参照カウンタを使わない=Objective-Cの参照カウンタを使う、です。

ポインタのビットのうちのobjectPointerIsObjCBitというのを見てそれをチェックしています。
https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2024-07-15-a/stdlib/public/runtime/SwiftObject.mm#L569-L575

ということで、このビットが立っていればObjective-Cのオブジェクトのポインタ、そうでなければSwiftのオブジェクトのポインタと見なします。

Swiftオブジェクトなら、フラグ類を落としたポインタ(=objectRef)に対してswift_retain()を呼びます。
Objective-Cオブジェクトならフラグ類を落とす前のポインタ(=object)に対してobjcRetainAndReturn()を呼びます。

kabeyakabeya

なんだそりゃ。

の答えは、コメントに書いてありました。

  // 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が速くなる、というように読めます。