Swift Digest
SE-0495 | Swift Evolution

C compatible functions and enums

Proposal
SE-0495
Authors
Alexis Laferrière
Review Manager
Steve Canon
Status
Implemented (Swift 6.3)

01 何が問題だったのか

Swift はもともと C との連携手段として、C ヘッダの import や C 関数の呼び出しをサポートしてきました。一方、Objective-C との連携はさらに踏み込んでいて、ヘッダの import や呼び出しに加え、Swift 側で書いた API を Objective-C 向けに公開する compatibility header の生成や、@implementation を使って Objective-C のクラスの実装を Swift で書く(SE-0436)といった機能まで揃っています。

ところが「逆方向」、つまり Swift で実装した関数を C から呼び出す ための公式な手段がありませんでした。

実際には長年、非公式の @_cdecl 属性が使われてきました。これは _ 始まりが示す通り experimental な機能で、正式な言語機能ではないままユーザーに広く使われている状態でした。

// 非公式かつ experimental な書き方
@_cdecl("mirror")
func mirror(value: CInt) -> CInt { return value }

@_cdecl には次のような問題がありました。

  • 公式の言語機能ではないため、振る舞いや制約が保証されていない。
  • 関数のシグネチャに使える型のチェックが緩く、C で表現できない型を書いてしまっても気づきにくい。とくに Objective-C が利用できる環境で開発していると、C 単独の環境ではコンパイルできない型を誤って混入させやすい。
  • Swift コンパイラが生成する compatibility header は Objective-C と C++ 向けのブロックしか持たず、@_cdecl で公開した関数の C 宣言を出力する仕組みがない。そのため C 側からその関数を呼ぶには、対応する宣言を手書きで用意し、Swift 側のシグネチャと食い違わないように人手で揃え続ける必要がある。
  • Swift 側で「この関数は C 側のあの宣言の実装です」と紐付ける手段がなく、C ヘッダ中の宣言と Swift 実装のシグネチャが一致しているかをコンパイラが検証してくれない。

C 言語のコードベースに Swift を少しずつ取り入れていきたい、あるいは Swift で書いた処理を C の世界へ素直に公開したい、という用途に対して、型チェックとヘッダ生成を伴った正式な C 互換 API 公開手段 が必要でした。

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

非公式な @_cdecl を置き換える正式な仕組みとして、新しい属性 @c を導入し、既存の @objc をグローバル関数にも適用できるように拡張します。さらに @implementationSE-0436)をグローバル関数へも広げ、@c / @objc の関数を C / Objective-C ヘッダ側の宣言と紐付けられるようにします。

@c グローバル関数

@c をグローバル関数に付けると、その関数は C の呼び出し規約(C calling convention)で呼べる関数 として扱われ、シグネチャに使える型は C で表現可能なものに限定されます。本体は通常の Swift で書けます。C 側に公開される名前は、デフォルトでは Swift の関数名がそのまま使われ、属性に引数を渡すことで別名にもできます。

@c func foo() {}

@c(mirrorCName)
func mirror(value: CInt) -> CInt { return value }

C で表現可能な型としては、Int / Bool / Float / Double などのプリミティブ、CInt / CLong などの C プリミティブの Swift 版、OpaquePointer や各種 Unsafe...Pointer@convention(c) の関数参照、SIMD 型(スカラーが C 表現可能な場合)、後述する @c enum、import された C の型などが認められます。一方で、ポインタ以外の Optional、非 @objc クラス、Swift の構造体、@c でない enum、protocol existential などは型チェックで弾かれます。

@objc グローバル関数

これまで型・メソッドにのみ付けられた @objc を、グローバル関数にも付けられるようにします。挙動は @c と同じく C の呼び出し規約での呼び出しを可能にしますが、シグネチャには @objc クラスや import された Objective-C 型など Objective-C で表現可能な型 も使えます。compatibility header には Objective-C ブロックの中で出力されます。

@objc func bar() {}

@objc(mirrorObjCName)
func objectMirror(value: NSObject) -> NSObject { return value }

非公式な @_cdecl の素直な置き換え先は基本的にこちらの @objc です。@_cdecl の振る舞いをそのまま保ちたい場合は @objc に、より厳しい C 互換チェックをかけたい場合は @c に書き換えます。

@c enum

@c は enum にも付けられます。@c enum は C 互換とみなされ、@c / @objc グローバル関数のシグネチャから参照できます。raw 値の型は C 互換な整数型(CInt など)でなければなりません。

@c
enum CEnum: CInt {
    case first
    case second
}

compatibility header には、属性で指定した C 名(指定がなければ Swift 名)で出力され、各ケースは「enum 名 + ケース名」を連結した名前(先頭は自動で大文字化)で出力されます。上記の例では CEnumFirstCEnumSecond という C の定数名になります。

なお、@objc enum はもともと利用できるので、@objc グローバル関数のシグネチャからは引き続きそのまま使えます(ただし @c 関数からは使えません)。

compatibility header への出力

Swift コンパイラが生成する compatibility header に、新たに C 専用のブロック が追加されます。@c 関数や @c enum はこのブロックに、C の型を使った宣言として出力されます。@objc グローバル関数は従来通り Objective-C ブロック側に Objective-C 型で出力されます。

ヘッダの生成は既存のオプション(-emit-objc-header-emit-objc-header-path-emit-clang-header-path)でそのまま要求でき、C 専用フラグは追加されません。C ブロックは C / Objective-C / C++ のいずれのコンパイラからも parse できる形で出力されます。

C 側に出力されるときの型対応は、たとえば次のようになります。

  • Swift の Bool → C の boolstdbool.h
  • Swift の Intptrdiff_tUIntsize_tInt8int8_tUInt32uint32_t など
  • Float / Double → C の float / double
  • CInt / CLong などの C プリミティブの Swift 版 → 対応する C 型(intlong、…)
  • @convention(c) の関数参照 → C の関数ポインタ
  • SIMD 型 → 必要な vector 型として compatibility header に出力

@c @implementation / @objc @implementation グローバル関数

@implementation をグローバル関数にも適用できるように拡張します。これは「C / Objective-C のヘッダにすでに宣言されている関数の実装を、Swift 側で提供する」ための書き方です。

// C ヘッダ
int cImplMirror(int value);
// Swift 側
@c @implementation
func cImplMirror(_ value: CInt) -> CInt { return value }

@implementation を付けた関数については、ヘッダ側の宣言と Swift 側の実装でシグネチャが一致しているかをコンパイラが検証します。手書きヘッダと Swift 実装の食い違いをコンパイル時に拾えるので、C のコードベースに Swift で実装を書き足していくような場面で有用です。@implementation が付いた関数は compatibility header には出力されません(宣言は手書きヘッダ側にあるため)。

@_cdecl からの移行と ABI

ABI の観点では、@c@objc のグローバル関数は C の呼び出し規約に従う 単一のシンボル を出力します。これに対し非公式な @_cdecl は、C 用と Swift 用の2つのシンボルを出していました。そのため、

  • @c@objc の付け外しは ABI 破壊的。
  • @c@objc の相互の入れ替えは ABI 互換。
  • @_cdecl@c / @objc の入れ替えは ABI 破壊的(出力シンボル数が変わるため)。

@_cdecl を使ってきたコードを移行する場合、振る舞いを変えたくなければ @objc に置き換えるのが基本です。より厳しい C 互換チェックを得たいなら @c に置き換えますが、compatibility header での C 名の出力のされ方が変わるため、呼び出し側の C コードの修正が必要になることがあります。

Future Directions(参考)

提案では、今回のスコープから外した方向性として、@c を struct にも広げること(C へ opaque な参照として公開する案と、C のメモリレイアウトを持った struct として公開する案の2つが議論されています)や、@c @implementation 経由で C ヘッダ側に書いた属性を活用することを足がかりにした、stdcall などのカスタム呼び出し規約への対応が speculative に挙げられています。あくまで今後の方向性を示すもので、実現を約束するものではありません。