Import as member
01 何が問題だったのか
Swift は C のヘッダを読み込んで、関数や変数をそのまま Swift から呼び出せるようにする仕組みを備えています。しかし import された C の API は、Swift のネイティブな API と比べて使い勝手が悪く、違和感のある書き方を強いられていました。
グローバル関数・グローバル変数としてしか import できない
C の API はグローバル関数やグローバル変数として宣言されているため、Swift でもそのままグローバルな関数・変数として見えます。たとえば Core Graphics を使った描画コードは次のように書く必要がありました。
override func drawRect(rect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
var transform = CGAffineTransformIdentity
transform = CGAffineTransformTranslate(transform, dx, dy)
transform = CGAffineTransformRotate(transform, angle)
CGContextSetLineWidth(context, bounds.size.width / 100)
CGContextSetGrayStrokeColor(context, 0.5, 1.0)
CGContextDrawPath(context, .Stroke)
}
型にまつわる操作が CGAffineTransformTranslate や CGContextSetLineWidth のようなプレフィックス付きのグローバル関数として散らばっており、Swift のネイティブな型が持つメソッドやプロパティ、イニシャライザのように扱うことができません。本来なら transform.translate(...) や context.lineWidth = ... のように書きたいところです。
回避策は重複を生むラッパーやオーバーレイしかなかった
C のフレームワークをネイティブな Swift らしく見せるためには、フレームワーク側で Swift 用のラッパー(オーバーレイ)を別途用意するしかありませんでした。しかしこの方法には次のような欠点があります。
- C ヘッダとは別にラッパーの API を維持する必要があり、二重管理になる。
- 呼び出しが C API を直接叩かず、ラッパー関数を経由するため、余分な関数呼び出しが挟まる。
特に CF 系のように命名規則が統一されている大規模な C API では、一つひとつ手作業でラッパーを書くコストが大きく、現実的ではありませんでした。
既存の NS_SWIFT_NAME では表現力が足りない
従来も NS_SWIFT_NAME 属性で Swift 側での見え方を調整することはできましたが、指定できるのは名前の付け替え程度で、「C のグローバル関数を、ある型のメソッド・イニシャライザ・プロパティとして import する」といった構造の変更はできませんでした。その結果、C API の作者が Swift 利用者向けにできる調整は限定的なものにとどまっていました。
02 どのように解決されるのか
C のグローバル関数や変数を、Swift 側では特定の型のメンバ(メソッド・イニシャライザ・プロパティ・subscript)として import できるようにします。これは主に swift_name 属性による手動指定 と、命名規則からの自動推論 の2本柱で実現されます。import された API は余分なラッパーを介さず、元の C 関数を直接呼び出すようにコード生成されます。
swift_name 属性による手動指定
C ヘッダ側で swift_name 属性(Core Foundation 系では CF_SWIFT_NAME マクロ)を使い、Swift 側での見え方を細かく指定できます。属性の文字列では次の記法を用います。
型名.メンバ名(引数ラベル:...)の形で、どの型のメンバとして import するかを指定する。- 引数ラベルに
selfを使うと、その引数をインスタンス自身として扱うインスタンスメンバになる。selfがない場合は型メンバ(static)になる。 - 先頭に
getter:/setter:を付けると、関数を computed property のゲッタ/セッタとして import する。 - subscript のセッタでは
newValueを使って、どの引数が新しい値かを示す。
// イニシャライザとして import
struct Point3D createPoint3D(float x, float y, float z)
__attribute__((swift_name("Point3D.init(x:y:z:)")));
// インスタンスメソッドとして import(第1引数を self として扱う)
struct Point3D rotatePoint3D(Point3D point, float radians)
__attribute__((swift_name("Point3D.rotate(self:radians:)")));
// インスタンスの computed property として import
float Point3DGetRadius(Point3D point)
__attribute__((swift_name("getter:Point3D.radius(self:)")));
void Point3DSetRadius(Point3D point, float radius)
__attribute__((swift_name("setter:Point3D.radius(self:_:)")));
// 型のプロパティ(static な stored property 相当)として import
extern struct Point3D identityPoint
__attribute__((swift_name("Point3D.identity")));
// 型の computed property として import
Point3D getZeroPoint(void)
__attribute__((swift_name("getter:Point3D.zero()")));
void setZeroPoint(Point3D point)
__attribute__((swift_name("setter:Point3D.zero(_:)")));
// subscript として import
float Point3DGetPointAtIndex(int idx, Point3D point)
__attribute__((swift_name("getter:subscript(_:self:)")));
void Point3DSetPointAtIndex(int idx, Point3D point, float val)
__attribute__((swift_name("setter:subscript(_:self:newValue:)")));
プロトコルに対してインスタンスメンバを追加することも可能ですが、その場合はプロトコル拡張として import されるため static dispatch になり、static メソッドやイニシャライザを付け加えることはできません。また、swift_name はプロトタイプなしの関数宣言には付けられません。
自動推論
CF 系のように一貫した命名規則で書かれた C API については、swift_name を一つひとつ書かなくても、Swift 側が関数名と型から自動的に適切なメンバへ振り分ける仕組みも用意されます。自動推論は opt-in で、デフォルトではすべての C API に対して適用されません。
推論の主なパターンは次のとおりです。
- 戻り値の型からイニシャライザを見出す:
CGColorCreate(space:_:)のように、特定の型を返すCreate系関数は、その型のイニシャライザCGColor.init?(space:components:)として import する。 Get/Setのペアから computed property を作る:CGContextGetInterpolationQualityとCGContextSetInterpolationQualityのペアは、CGContextのinterpolationQualityプロパティにまとめる。- ブーリアンの述語関数を
is〜形式のプロパティにする:CGDisplayModeIsUsableForDesktopGUIはCGDisplayMode.isUsableForDesktopGUIとして見える。 - self に対応する引数を見つけてメソッド化する:
CGAffineTransformInvert(t:)はCGAffineTransform.invert()になる。 - 型 ID のような特殊パターンは型プロパティとして扱う:
CGDisplayStreamUpdateGetTypeIDはCGDisplayStreamUpdate.typeIDになる、など。
直接 C を呼ぶコード生成
import されたメンバは元の C 関数への糊付けとしてふるまうだけで、中間のラッパー関数を介しません。インスタンスメンバの場合は、self を C 側の対応する引数位置に渡すコードが SILGen で直接生成されます。これにより、Swift らしい書き味と C 呼び出しとしての効率の両方が保たれます。
実際の書き味の変化
この仕組みにより、先ほどの Core Graphics の例は次のように書けるようになります。
override func drawRect(rect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
var transform = CGAffineTransform.identity
transform = transform.translate(toX: dx, toY: dy)
.rotate(angle: angle)
context.lineWidth = bounds.size.width / 100
context.strokeColor = CGColor(gray: 0.5, alpha: 1.0)
context.drawPath(mode: .Stroke)
}
グローバル関数の羅列だったコードが、Swift のネイティブな型が持つメソッド・プロパティ・イニシャライザを呼び出す形になり、ネイティブな Swift API と区別のつかない書き味になります。
既存コードへの影響
この機能を活用したフレームワークを利用している Swift コードは、従来のグローバル関数呼び出しから新しいメンバ呼び出しへ書き換える必要があります。ただし import は Clang Importer 側でプログラム的に行われるため、新旧の対応関係を migration 属性として付与でき、Swift migrator による機械的な変換が可能です。