SwiftのToll-Free Bridgeの実装を読む
Toll-Free Bridgeとは
Objective-CにはToll-Free Bridgeという仕組みがありました。
これは、C言語のライブラリであるCoreFoundationと、
Objective-CのライブラリであるFoundationの間で、
対応するクラス同士でバイナリ互換性があり、
ポインタを強制キャストすることでどちらにもなる、という不思議なものです。
下記にNSDataをCFDataとして扱うサンプルを示します。
// コンパイル: 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として扱うサンプルを示します。
// コンパイル: 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 *
|
} | } |
また関連ソースは下記のとおりです。
- swift-corelibs-foundation(FoundationとCoreFoundation)
- NSData.swift
- NSObjCRuntime.swift(_CFInfo)
- CFData.h
- CFData.c
- CFRuntime.h(CFRuntimeBase, CFRetain)
- swift(Swiftコンパイラとランタイム)
- include/../HeapObject.h(swift_retain)
- stdlib/../HeapObject.h(HeapObject)
- stdlib/../RefCount.h(StrongRefCount, WeakRefCount)