Make unsafe pointer nullability explicit using Optional
01 何が問題だったのか
Swift 2 までの UnsafePointer<T> や UnsafeMutablePointer<T>、AutoreleasingUnsafeMutablePointer<T>、OpaquePointer といった各種のポインタ型は、C から受け継いだ性質として、どの値も「null であり得る」ものとして扱われていました。どの UnsafePointer<Int> 値も null になり得る可能性があり、ポインタを扱うコード側も、呼び出し先が null を想定しているのか、そうでないのかを型から読み取ることができませんでした。
null の見落としがクラッシュに直結する
UnsafePointer に対するほぼすべての基本操作(pointee の読み書きや初期化、ポインタ演算など)は、有効なポインタを前提にしています。そのため、呼び出し元が null チェックをし忘れると、アサーション失敗やクラッシュに直結します。しかし型システム上は「null を受け付ける API」と「受け付けない API」を区別できないため、どこで null を警戒すべきなのかがコードから読み取りにくい状態でした。
NilLiteralConvertible の濫用
さらに、各ポインタ型はいずれも NilLiteralConvertible に適合していて、nil を直接代入できるようになっていました。これに Objective-C 由来の Selector や NSZone なども加わり、NilLiteralConvertible に適合する型が言語全体に広がっていました。
// Swift 2: どのポインタにも nil を直接代入できた
let p: UnsafePointer<Int> = nil
この状態は、「nil は Optional のものだ」という素直な理解で Swift を学び始めた利用者にとって、混乱の元になっていました。
Objective-C の nullability 情報が活かせない
Apple の Objective-C ヘッダでは、オブジェクト型だけでなく非オブジェクトのポインタ型にも _Nullable / _Nonnull といった nullability 注釈が付けられています。しかし、Swift 側のポインタ型がすべて「null になり得る」扱いだったために、せっかく C / Objective-C 側で表明されている情報が Swift の型には反映されず、バグ予防に活かせませんでした。
C との相互運用で null 終端配列が表現できない
ポインタの要素型そのものがポインタになるケース(たとえば C の char **)では、「配列の終端を null で表す」という慣用的なレイアウトがよく使われます。しかし Swift 2 の UnsafeMutablePointer<UnsafeMutablePointer<Int8>> では、内側のポインタが null になり得ることが型には現れず、利用者が自前で規約として守る必要がありました。たとえば Process.unsafeArgv の型もそのような曖昧なシグネチャになっていました。
02 どのように解決されるのか
Swift が既に持っている仕組みである Optional を使って、ポインタの nullability を型に反映させます。
UnsafePointer<T>をはじめとする各ポインタ型は、常に非 null であることを表す型とします。- null になり得るポインタは、
UnsafePointer<T>?のようにOptional化して表します。 - ポインタ型から
NilLiteralConvertible適合を外し、nilはOptionalを介してのみ表現されるようにします。 - Clang インポータを更新し、Objective-C ヘッダの
_NullableポインタはOptional、_Null_unspecifiedポインタはImplicitlyUnwrappedOptional(T!)として取り込みます。
対象となるポインタ型は次の通りです。
UnsafePointerUnsafeMutablePointerAutoreleasingUnsafeMutablePointerOpaquePointerSelectorNSZone
この変更によって、NilLiteralConvertible に適合するのは Optional、ImplicitlyUnwrappedOptional、そして比較用の内部型だけになり、「nil は Optional のためのもの」という原則に揃います。
使い方
宣言の型からそのまま nullability が読み取れるようになります。
// 常に有効なポインタを受け取る API
func load(from p: UnsafePointer<Int>) -> Int {
return p.pointee
}
// null を許容する API
func loadIfAvailable(from p: UnsafePointer<Int>?) -> Int? {
guard let p = p else { return nil }
return p.pointee
}
Optional になったことで、ポインタに対して ?. の syntactic sugar がそのまま使えます。従来は標準ライブラリ内部で _setIfNonNil のようなヘルパーを用意していたものが、次のように素直に書けるようになります。
// Before (Swift 2)
if ptr != nil { ptr.pointee = newValue }
// After (Swift 3)
ptr?.pointee = newValue
init(bitPattern:) は失敗可能に
整数値からポインタを作る init(bitPattern: Int) / init(bitPattern: UInt) は、失敗可能イニシャライザ(init?)に変わります。ビットパターンが 0(null ポインタ)だった場合は nil が返ります。
let p = UnsafePointer<Int>(bitPattern: address) // UnsafePointer<Int>?
ポインタ間の変換
要素型を変えるだけの変換(アドレスはそのまま)のために、次のような非 null → 非 null のイニシャライザが従来から提供されていました。
init<OtherPointee>(_ otherPointer: UnsafePointer<OtherPointee>)
これに加えて、オプショナルなポインタを受け取り、オプショナルなポインタを返す失敗可能イニシャライザが追加されます。
init?<OtherPointee>(_ otherPointer: UnsafePointer<OtherPointee>?)
これにより、オプショナル性を保ったままの変換が自然に書けます。
// Before (Swift 2)
let untypedPointer = UnsafePointer<Void>(ptr)
// After (Swift 3): ptr が非 null ならそのまま
let untypedPointer = UnsafePointer<Void>(ptr)
// Optional 経由でも map を挟まずに書ける
func forward(_ p: UnsafePointer<Int>?) {
let q = UnsafePointer<Void>(p) // UnsafePointer<Void>?
// ...
}
UnsafeBufferPointer の baseAddress
UnsafeBufferPointer / UnsafeMutableBufferPointer は、先頭アドレス(baseAddress)と要素数(count)でバウンド付きの領域を表すコレクション型です。要素数 0 のバッファでは、実メモリを確保せず baseAddress を null、count を 0 とする運用が一般的でした。この実情に合わせて、baseAddress と対応するイニシャライザ引数がオプショナルになります。
// Before (Swift 2)
public init(start: UnsafePointer<Element>, count: Int)
public var baseAddress: UnsafePointer<Element> { get }
// After (Swift 3)
public init(start: UnsafePointer<Element>?, count: Int)
public var baseAddress: UnsafePointer<Element>? { get }
start が nil の場合は count を必ず 0 にする、という制約は変わりません。多くのコードは Collection 適合経由でバッファを使うか、(ポインタ, 長さ) のペアをそのまま C API に渡すだけなので、null ポインタを扱っても実害は出にくい、という判断に基づいています。
影響を受ける標準ライブラリ API
Optional が型に現れるため、いくつかの API のシグネチャが変わります。代表例は次の通りです。
Process.unsafeArgvの型がUnsafeMutablePointer<UnsafeMutablePointer<Int8>>からUnsafeMutablePointer<UnsafeMutablePointer<Int8>?>に変わり、null 終端の C 文字列配列であることが型から読み取れるようになります。NSErrorPointerはAutoreleasingUnsafeMutablePointer<NSError?>からAutoreleasingUnsafeMutablePointer<NSError?>?に変わります(外側もオプショナル)。NSString由来でUnsafeMutablePointer<...>を既定値nilで受け取っていたStringの各種メソッド(completePathIntoString(...)、init(contentsOfFile:usedEncoding:)、linguisticTags(in:scheme:options:orthography:tokenRanges:)など)は、引数型がUnsafeMutablePointer<...>?に変わります。NSZoneの引数なしイニシャライザは削除されます。
既知の注意点
可変長引数(C variadic)として withVaList にオプショナルなポインタを直接渡すことはできなくなります。CVarArg への条件付き適合が必要になるためです。当面は unsafeBitCast で Int に落として渡すのが推奨の回避策です。Int は主要プラットフォームでポインタと同じ C variadic 呼び出し規約を持つため、この用途では安全に使えます。
// Optional なポインタを可変長引数に渡したいとき
let arg = unsafeBitCast(optionalPointer, to: Int.self)
withVaList([arg]) { /* ... */ }