Swift Digest
SE-0324 | Swift Evolution

Relax diagnostics for pointer arguments to C functions

Proposal
SE-0324
Authors
Andrew Trick, Pavel Yaskevich
Review Manager
Saleem Abdulrasool
Status
Implemented (Swift 5.6)

01 何が問題だったのか

Swift から C の関数を呼び出す際、ポインタ引数の型が Swift 側とぴったり一致していないと、たとえ C の規則上は安全な呼び出しであってもコンパイルエラーになっていました。

C にはポインタのエイリアシングに関する特別な規則があります。たとえば char * は他のポインタ型と別名になることが許され、また符号の違いだけが異なる整数型へのポインタ同士も別名にできます。C の API はしばしばこの規則に依存して作られており、呼び出し側が素直にバイト列を渡せるように char *unsigned char * を受け取る設計になっています。

一方で Swift は型付きポインタの変換を一般には許しません(SE-0107 を参照)。そのため、C 関数に UnsafeRawPointerUnsafePointer<T> を渡そうとすると、次のような不親切なエラーに出会います。

error: cannot convert value of type 'UnsafeRawPointer' to expected argument type 'UnsafePointer<UInt8>'

たとえば Data のバイト列を Foundation の OutputStream.write(_:maxLength:) に渡したいだけの次のコードが、そのままではコンパイルできません。

func write(messageData: Data, output: OutputStream) -> Int {
  return messageData.withUnsafeBytes { rawBuffer in
    guard let rawPointer = rawBuffer.baseAddress else { return 0 }
    return output.write(rawPointer, maxLength: rawBuffer.count)
  }
}

この壁を越えるために、多くのプログラマは UnsafeRawPointer の「memory binding」API である bindMemory(to:capacity:)assumingMemoryBound(to:) に手を伸ばしてしまいます。しかしこれらはもともと低レベルライブラリのために用意されたもので、通常の Swift プログラミングで使うことは想定されていません。呼び出し側でメモリの使われ方全体を把握していないと正しく使えず、実際に誤用されているケースも多く、将来ランタイムサニタイザが導入されれば未定義動作として検出されるものも含まれます。

// よく見かける「とりあえず通す」ための書き方。誤用になりやすい
func write(messageData: Data, output: OutputStream) -> Int {
  return messageData.withUnsafeBytes { rawBuffer in
    guard let rawPointer = rawBuffer.baseAddress else { return 0 }
    let bufferPointer = rawPointer.assumingMemoryBound(to: UInt8.self)
    return output.write(bufferPointer, maxLength: rawBuffer.count)
  }
}

同様の問題は、圧縮や暗号関連の C API(CommonCrypto など)や、符号あり/符号なしの違いだけが問題となる API(mach カーネルの task_info など)を呼ぶ際にも繰り返し発生していました。char * を介したバイト列のやり取りや、符号だけが異なるポインタのやり取りは、C の規則上は安全にもかかわらず、Swift 側の型チェックが阻んでしまい、初心者が詰まりやすい定番のポイントになっていました。

02 どのように解決されるのか

C/Objective-C/C++ のヘッダからインポートされた関数を呼び出すときに限り、C のエイリアシング規則が許す範囲でポインタ引数の暗黙変換を行えるようにします。Swift 自身の関数呼び出しではこの緩和は適用されないため、Swift の型付きポインタのモデルはこれまでどおり保たれます。

許容される暗黙変換は次の 2 種類です。

char * 相当への変換

生ポインタまたは任意の型付きポインタ Unsafe[Mutable]RawPointer / Unsafe[Mutable]Pointer<T1> は、要素型が Int8 または UInt8 の型付きポインタ Unsafe[Mutable]Pointer<T2> へ暗黙に変換できます。これにより、C で char * / signed char * / unsigned char * として宣言されたポインタ型に素直にバイト列を渡せます。

ただしこの方向の変換は非対称(commutative でない)です。つまり UnsafeRawPointer から UnsafePointer<UInt8> への変換は効きますが、その逆は働きません。

符号違いの整数ポインタ同士の変換

Unsafe[Mutable]Pointer<T1>Unsafe[Mutable]Pointer<T2> は、T1T2 が符号のみ異なる同じ整数型のときに相互変換できます。具体的には Int8UInt8Int16UInt16Int32UInt32Int64UInt64 の組です。こちらは双方向(commutative)に変換できます。

変換早見表

SE-0324 の対応表を整理すると次のようになります。「Actual」は Swift 側の実引数の型、「Parameter」は C からインポートされた関数の仮引数の型です。

Swift 側の引数 C から来た仮引数 双方向か
UnsafeRawPointer UnsafePointer<[U]Int8> No
UnsafeMutableRawPointer Unsafe[Mutable]Pointer<[U]Int8> No
UnsafePointer<T> UnsafePointer<[U]Int8> No
UnsafeMutablePointer<T> Unsafe[Mutable]Pointer<[U]Int8> No
UnsafePointer<IntN> UnsafePointer<UIntN> Yes
UnsafeMutablePointer<IntN> Unsafe[Mutable]Pointer<UIntN> Yes

使い方

Motivation で挙げた OutputStream.write(_:maxLength:) の呼び出しは、そのまま書けるようになります。assumingMemoryBound(to:) で無理やり型を合わせる必要はありません。

func write(messageData: Data, output: OutputStream) -> Int {
  return messageData.withUnsafeBytes { rawBuffer in
    guard let rawPointer = rawBuffer.baseAddress else { return 0 }
    // UnsafeRawPointer を UnsafePointer<UInt8> へ暗黙変換して渡せる
    return output.write(rawPointer, maxLength: rawBuffer.count)
  }
}

自作の C ヘッダを呼ぶケースも同様です。たとえば次のような C 関数を考えます。

// encrypt.h
#include <stddef.h>

struct DigestWrapper {
  unsigned char digest[20];
};

int computeDigest(unsigned char *output,
                  const unsigned char *input,
                  size_t length);

Swift からの呼び出しは、生ポインタをそのまま渡すだけで済みます。

func makeDigest(data: Data, wrapper: inout DigestWrapper) -> Int32 {
  data.withUnsafeBytes { inBytes in
    withUnsafeMutableBytes(of: &wrapper.digest) { outBytes in
      // UnsafeRawPointer / UnsafeMutableRawPointer が
      // それぞれ const unsigned char * / unsigned char * に暗黙変換される
      computeDigest(outBytes.baseAddress, inBytes.baseAddress,
                    inBytes.count)
    }
  }
}

Optional にくるまれたポインタ型(例: UnsafePointer<UInt8>?)を期待する引数にも、まず optional への昇格やアンラップが試され、その後にこの新しい暗黙変換が適用されます。そのため baseAddress のような Optional を返すプロパティも自然に渡せます。

適用範囲

この緩和は C/Objective-C/C++ からインポートされた関数の引数位置だけに働きます。Swift で書かれた関数の呼び出しや、C API の関数ポインタ引数には適用されません。関数ポインタを介するケースは比較的まれで、必要なら Swift 側に小さなシムを用意するのが素直です。また、C コンパイラはもともとこれらのポインタ型が互いに別名になり得る前提でコードを生成しているため、この変換を許しても型安全性が新たに損なわれることはありません。