Posted at

SwiftのToll-Free Bridgeの実装を読む

More than 3 years have passed since last update.


SwiftのToll-Free Bridgeの実装を読む


Toll-Free Bridgeとは

Objective-CにはToll-Free Bridgeという仕組みがありました。

これは、C言語のライブラリであるCoreFoundationと、

Objective-CのライブラリであるFoundationの間で、

対応するクラス同士でバイナリ互換性があり、

ポインタを強制キャストすることでどちらにもなる、という不思議なものです。

下記にNSDataをCFDataとして扱うサンプルを示します。


a.m

// コンパイル: clang -framework Foundation a.m

#import <Foundation/Foundation.h>

int main(int argc, char * argv[]) {
@autoreleasepool {
NSData * data = [[NSData alloc] initWithBytes: "hello" length: 5];
CFDataRef cfData = (CFDataRef)data;
int len = CFDataGetLength(cfData);
NSLog(@"len=%d\n", len);
}
}

// 出力:
// len=5


下記にCFDataをNSDataとして扱うサンプルを示します。


b.m

// コンパイル: clang -framework Foundation b.m

#import <Foundation/Foundation.h>

int main(int argc, char * argv[]) {
@autoreleasepool {
CFDataRef cfData = CFDataCreate(NULL, (uint8_t *)"hello", 5);
NSData * data = (NSData *)cfData;
int len = CFDataGetLength(cfData);
NSLog(@"len=%d\n", len);
CFRelease(cfData);
}
}

// 出力:
// len=5


このように、NSData *CFDataRefを、ポインタキャストすることで相互に変換可能です。1


SwiftでのToll-Free Bridge

先日Swiftがオープンソース化されました。2

Foundationのソースも公開されたので見てみると、下記のような事が書いてあります。

フレームワークの内部設計の鍵は、CoreFoundationの実装とSwiftによる実装に、

Foundationのクラスを分割していることです。

(略)

一部の少数のクラスはトールフリーブリッジという特別な相互の関係を持っています。
CFTypeRefがFoundationのクラスとトールフリーブリッジにあるとき、
そのポインタは対応する型に単純にキャストでき、その型を受ける関数やメソッドに渡すことができます。

トールフリーブリッジが動くように、
SwiftのクラスとCoreFoundationのstructは全く同じメモリレイアウトを共有しなければなりません。

原文

これまでのFoundationはObjective-Cのライブラリでしたが、

これからはFoundationはSwift実装になり、

けれども一部はこれまでのようにToll-Free Bridgingされるというのです。

この辺りの仕組みがどうなっているのかを、ソースを追いかけて調べました。3


NSDataの実装

FoundationやCoreFoundationのソースは、swift-corelibs-foundationというリポジトリにあります。

NSDataの実装、FoundationのNSData.swiftを見てみます。

クラスローカルな型名として、CFDataRefをCFTypeと命名しています。

    typealias CFType = CFDataRef

興味深いプロパティ_cfObjectがあります。

NSData自身のCoreFoundation上での姿、CFDataRefを返しています。

    internal var _cfObject: CFType {

get {
if self.dynamicType === NSData.self || self.dynamicType === NSMutableData.self {
return unsafeBitCast(self, CFType.self)
} else {
return CFDataCreate(kCFAllocatorSystemDefault, unsafeBitCast(self.bytes, UnsafePointer<UInt8>.self), self.length)
}
}
}

だいたいtrueになりそうなif分岐があるので中を見てみると、unsafeBitCastでselfをCFTypeに変換しています。

unsafeBitCastはバイナリそのままで型だけ強引にキャストする命令です。

CFTypeはCFDataRefのエイリアスでしたから、

ここがまさにNSDataのポインタからCFDataRefに直接キャストしている部分になります。

Swiftでもトールフリーブリッジが実現されている事がわかります。

NSDataの他のプロパティ、lengthやbytesを見てみると、

これらの実装は、さっきのトールフリーブリッジされた自分自身を通して、

CoreFoundationの実装を呼び出すようになっていることがわかります。

    public var length: Int {

get {
return CFDataGetLength(_cfObject)
}
}

public var bytes: UnsafePointer<Void> {
get {
return UnsafePointer<Void>(CFDataGetBytePtr(_cfObject))
}
}

FoundatioのNSDataの実装が、CoreFoundationのCFDataの実装を利用する形でなされている事がわかりました。

このような設計であれば、CoreFoundationをLinux, Windows向けにクロス対応をすれば、

Swiftで書かれたFoundationの部分は小さな負担でクロス対応できそうです。

Swift自体にもクロスコードを書く機能はありますし、Cのコードを呼ぶ機能もありますが、

そもそものシステムライブラリがC向けに提供されている以上、

クロスプラットフォーム化の作業はC言語で行うのが比較的やりやすそうです。

うまい設計だと思います。

NSDataに書かれていたCFDataRefは、

C言語で書かれたシンボルで、それをSwiftから参照しています。

その定義はCoreFoundationのCFData.hにあります。

構造体__CFDataへのポインタをCFDataRefと名づけてあります。

これで、unsafeBitCastが、

やはりポインタからポインタへのキャストであることが確認されました。

typedef const struct CF_BRIDGED_TYPE(NSData) __CFData * CFDataRef;


CFDataの実装

SwiftのNSDataのインスタンスが、C言語の__CFData構造体と同じメモリレイアウトである事がわかりました。

C言語なら定義からメモリレイアウトを見ることができます。

CFDataのソース、CoreFoundationのCFData.cを見てみましょう。

__CFData構造体は次のような定義になっています。

struct __CFData {

CFRuntimeBase _base;
CFIndex _length; /* number of bytes */
CFIndex _capacity; /* maximum number of bytes */
CFAllocatorRef _bytesDeallocator; /* used only for immutable; if NULL, no deallocation */
#if DEPLOYMENT_RUNTIME_SWIFT
void *_deallocHandler; // for swift
#endif
uint8_t *_bytes; /* compaction: direct access to _bytes is only valid when data is not inline */
};

DEPLOYMENT_RUNTIME_SWIFTというマクロ分岐がありますね。

CoreFoundationにはSwift向けビルドとそうでない場合があるようです。

さて、上から順に_base,_length,_capacity,_bytesDeallocator,_deallocHandler,_bytesというフィールドが並んでいます。

NSDataのプロパティも見てみましょう。

    private var _base = _CFInfo(typeID: CFDataGetTypeID())

private var _length: CFIndex = 0
private var _capacity: CFIndex = 0
private var _deallocator: UnsafeMutablePointer<Void> = nil // for CF only
private var _deallocHandler: _NSDataDeallocator? = _NSDataDeallocator() // for Swift
private var _bytes: UnsafeMutablePointer<UInt8> = nil

同じ名前が並んでいる事が確認できます。

これにより、同じメモリレイアウトになっているようです。

まずswiftでNSDataのインスタンスを作り、

それをCFDataとしてキャストした時には、

CFDataが自身の_lengthだと思って見に行くメモリは、

もともとはSwift側の_lengthプロパティのために用意されたメモリ、

という風に動くわけです。

さて、NSDataにはバイナリを管理する以外に、

そもそも参照カウントを扱ったり、

メソッドのディスパッチのために自身のインスタンス情報を見たりする、

Swiftのクラスの基本機能が組み込まれているはずです。

一方、CoreFoundationにも参照カウントの機能があります。

Swiftで増やした参照カウントは、CoreFoundation側で見ても増える、というように、

連動しなければならないはずです。

このあたりの仕組みを見ていきましょう。


CFRuntimeBase

struct __CFDataの第一フィールドは、CFRuntimeBase _baseと書かれていました。

これの定義はCoreFoundationのソース、CFRuntime.hにあります。

CFRuntimeBaseの定義はマクロで分岐されているので、

#if DEPLOYMENT_RUNTIME_SWIFTを通る方を見ます。

typedef struct __CFRuntimeBase {

// This matches the isa and retain count storage in Swift
uintptr_t _cfisa;
uint32_t _swift_strong_rc;
uint32_t _swift_weak_rc;
// This is for CF's use, and must match _NSCFType layout
uint8_t _cfinfo[4];
uint8_t _pad[4];
} CFRuntimeBase;

上から、_cfisaというポインタ4_swift_strong_rcという32ビット整数、_swift_weak_rcという32ビット整数、

_cfinfoが4バイト、_padが4バイト、ということのようです。

では、NSDataの方の_baseを見てみましょう。

private var _base = _CFInfo(typeID: CFDataGetTypeID())

この_CFInfoというのは、Foundationのソース、NSObjCRuntime.swiftにあります。

internal struct _CFInfo {

// This must match _CFRuntimeBase
var info: UInt32
var pad : UInt32
init(typeID: CFTypeID) {
// This matches what _CFRuntimeCreateInstance does to initialize the info value
info = UInt32((UInt32(typeID) << 8) | (UInt32(0x80)))
pad = 0
}
init(typeID: CFTypeID, extra: UInt32) {
info = UInt32((UInt32(typeID) << 8) | (UInt32(0x80)))
pad = extra
}
}

これにはinfoが4バイト、padが4バイト定義されています。

これらの名前は、CFRuntimeBaseの5つのフィールドのうち、

最後の2つの_cfinfo_padに対応していそうです。

逆に言うと、CFRuntimeBaseの先頭3つのフィールドが、

SwiftのNSDataのソースには書かれていなかったということです。

なぜ書かれていないのかと言えば、

CoreFoundationはC言語ですから、

参照カウントなんかも自分でソースを書かなければなりませんが、

Swiftにおいてはそれは言語機能ですから、

Swiftのソースコードとしては書かなくても言語が提供してくれるからです。

CFRuntimeBaseの定義から考えると、

Swiftはクラスのインスタンスに、

ポインタ1つとカウンタ2つを持っているはずです。

このあたりを追いかけていきます。


CFRetain

CoreFoundationの参照カウント機能と、Swiftの参照カウント機能は、

Toll Free Bridgeされた時には、同じメモリであり同じコードであるはずです。

この部分のソースを見てみましょう。

CFRetainのソースはCoreFoundationのCFRuntime.cにあります。

これによると、CFRetain_CFRetainを呼び出すようです。

CFTypeRef CFRetain(CFTypeRef cf) {

if (NULL == cf) { CRSetCrashLogMessage("*** CFRetain() called with NULL ***"); HALT; }
if (cf) __CFGenericAssertIsCF(cf);
return _CFRetain(cf, false);
}

_CFRetainのソースは、マクロ分岐されていて長いですが、

DEPLOYMENT_RUNTIME_SWIFTの分岐だけで見るととても短いです。

static CFTypeRef _CFRetain(CFTypeRef cf, Boolean tryR) {

#if DEPLOYMENT_RUNTIME_SWIFT
// We always call through to swift_retain, since all CFTypeRefs are at least _NSCFType objects
swift_retain((void *)cf);
return cf;
#else
()
#endif
}

swift_retainという関数を呼び出すだけになっています。

そして、この関数に渡すときに、CFTypeRefをvoid *にキャストしています。

swift_retainの定義は、CFRuntime.cの中でextern定義されているだけです。

#if DEPLOYMENT_RUNTIME_SWIFT

extern void swift_retain(void *);
#endif

引数の型もvoid *になっており何もわかりません。

ソースはここには定義されていません。

リンクされるswiftのランタイム側にコードが入っていて、

CoreFoundationからはこの呼出ABIだけがわかっている、

という設計のようです。


swift_retain

swift_retainは、これまで見てきたswift-corelibs-foundationではなく、

swiftコンパイラのリポジトリの方に実装があります。

宣言はinclude/swift/Runtime/HeapObject.hにあります。

namespace swift {

()
extern "C" void swift_retain(HeapObject *object);
()
}

swiftネームスペースの中に定義されていますが、extern "C"なので、

C言語のswift_retainというシンボルになっています。

これがリンクされれば、さっきの_CFRetainからこれが呼ばれるわけです。

引数の型の定義もvoid *からHeapObject *に変わっています。

このHeapObjectを調べます。


HeapObject

HeapObjectの定義は、stdlib/public/SwiftShims/HeapObject.hにあります。

このヘッダは、さっきのincludeの方のHeapObject.hからも#includeされています。

// The members of the HeapObject header that are not shared by a

// standard Objective-C instance
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
StrongRefCount refCount; \
WeakRefCount weakRefCount

/// The Swift heap-object header.
struct HeapObject {
/// This is always a valid pointer to a metadata object.
struct HeapMetadata const *metadata;

SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
// FIXME: allocate two words of metadata on 32-bit platforms

#ifdef __cplusplus
HeapObject() = default;

// Initialize a HeapObject header as appropriate for a newly-allocated object.
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCount(StrongRefCount::Initialized)
, weakRefCount(WeakRefCount::Initialized)
{ }
#endif
};

C言語とC++のクロス定義なstructになっています。

1つ目のフィールドがHeapMetadata構造体へのポインタになっています。

その次は、すぐ上で定義されたマクロの展開になっていて、

マクロの展開結果として、StrongRefCount型とWeakRefCount型のフィールドが続いています。

この2つの型は、同じディレクトリのRefCount.hに定義されています。

C言語ではstruct,C++ではclassになるクロス定義になっています。

#if !defined(__cplusplus)


// These definitions are placeholders for importing into Swift.
// They provide size and alignment but cannot be manipulated safely there.

#include "SwiftStdint.h"

typedef struct {
__swift_uint32_t refCount __attribute__((unavailable));
} StrongRefCount;

typedef struct {
__swift_uint32_t weakRefCount __attribute__((unavailable));
} WeakRefCount;

// not __cplusplus
#else
// __cplusplus
()
class StrongRefCount {
uint32_t refCount;
()
};
()
class WeakRefCount {
uint32_t refCount;
()
};
()
#endif

CでもC++でも当然同じメモリレイアウトになっていて、

どちらもそれぞれ32ビットの整数を1つ持っていることがわかります。

よって、HeapObject構造体のメモリレイアウトは、

ポインタ1つに32ビット整数2つとなりました。

これでめでたくCFRuntimeBaseと一致することがわかりました。


メモリレイアウトまとめ

これまでのソースで、NSDataとCFDataのメモリレイアウトが一致し、

参照カウントの機能などはSwiftのランタイムコードが共用されることで、

Toll-Free Bridgeが実現されていることがわかりました。

改めてまとめると下記のようになります。

Foundation
CoreFoundation

__CFData {

HeapObject {
   _base: CFRuntimeBase {

   metadata: HeapMetadata *

     _cfisa: uintptr_t

   refCount: StrongRefCount {
    

     refCount: uint32_t

     _swift_strong_rc: uint32_t

   }
    

   weakRefCount: WeakRefCount {
    

     refCount: uint32_t

     _swift_weak_rc: uint32_t

   }
    

}
    

NSData {
    

   _base: __CFInfo {
    

     info: UInt32

     _cfinfo: uint8_t[4]

     pad: UInt32

     _pad: uint8_t[4]

   }
   }

   _length: CFIndex

   _length: CFIndex

   _capacity: CFIndex

   _capacity: CFIndex

   _deallocator: UnsafeMutablePointer<Void>

   _bytesDeallocator: CFAllocatorRef

   _deallocHandler: _NSDataDeallocator?

   _deallocHandler: void *

   _bytes: UnsafeMutablePointer<UInt8>

   _bytes: uint8_t *

}
}

また関連ソースは下記のとおりです。


注釈





  1. これらのサンプルでは、バイナリ互換性があることを伝えるためにC形式のポインタキャストを使用しました。実際のコードでは、CFBridgingRetainCFBridgingReleaseを使うべきです。 



  2. オープンソースになったものは、Swift3.0に向けた開発中のもので、現在Mac,iOS開発用にリリースされているものとは別だそうです。 



  3. 執筆時のmasterブランチにもとづいています。 



  4. 正確にはポインタと同じビット幅の整数、ですね。