この記事の要点
- Swift は C ライブラリを直接 import して使えますが、そのままだと「C をそのまま書いている」ような使い心地になり、グローバル関数・整数定数・unsafe なポインタ・手動の参照カウントといった C の不便さや危険さをそのまま引き継ぎます。
- C ヘッダを一切書き換えずに、Swift らしいインターフェイスを上から被せることができます。鍵となるのが Clang の API notes(
.apinotesという別ファイル)で、enum を本物のenumに、参照カウント型を自動参照カウントされるclassに、グローバル関数をメソッド・プロパティ・イニシャライザに変換できます。 - 独自の Bool 型やフラグ定数を型安全にしたり(
OptionSetへの適合)、ポインタの nullability を表現したりもできます。結果として、より安全で Swift らしい API を、ライブラリ本体に手を入れずに得られます。 - これらは特定のライブラリ専用の仕組みではなく、行儀のよい多くの C ライブラリに適用できる汎用的なテクニックです。
記事では WebGPU の C ヘッダ(webgpu.h)を題材に、次の「C そのまま」のコードを、
var instanceDescriptor = WGPUInstanceDescriptor()
let instance = wgpuCreateInstance(&instanceDescriptor)
var surfaceDescriptor = WGPUSurfaceDescriptor()
let surface = wgpuInstanceCreateSurface(instance, &surfaceDescriptor)
if wgpuSurfacePresent(&surface) == WGPUStatus_Error {
// report error
}
wgpuSurfaceRelease(surface)
wgpuInstanceRelease(instance)
最終的にこう書けるところまで持っていきます。
var instanceDescriptor = WGPUInstanceDescriptor()
let instance = WGPUInstance(descriptor: &instanceDescriptor)
var surfaceDescriptor = WGPUSurfaceDescriptor()
let surface = instance.createSurface(descriptor: &surfaceDescriptor)
if surface.present() == .error {
// report error
}
// instance と surface は不要になったら Swift が自動的に解放する
本記事の一部の機能は、Swift 6.2.3 で初めて利用可能になったバグ修正を必要とします。
背景: C ライブラリは「C のまま」入ってくる
C ライブラリを Swift から直接使うと、見た目も使い心地も C で使うときとほとんど同じになります。チュートリアルやサンプルコードが C で書かれている場合は便利ですが、次のような C の流儀がそのまま持ち込まれます。
wgpuInstanceCreateSurfaceのような接頭辞付きのグローバル関数WGPUStatus_Errorのようなグローバルな整数定数- 至るところで使われる unsafe なポインタ
AddRef/Release関数を自分で呼んでバランスさせる手動の参照カウント
動きはしますが Swift らしくはなく、C 由来の安全性の問題も抱えたままです。Swift は、C ヘッダに付けられる一連のアノテーションを用意していて、C のよくある慣習を Swift の構文に対応づけることで、より Swift に馴染むインターフェイスを上から投影できます。
セットアップ: module map と API notes
C ヘッダを Swift のモジュールとして使うには、まず module map を書きます。ヘッダと同じ場所に module.modulemap を置くのが手軽です。
module WebGPU {
header "webgpu.h"
export *
}
Swift パッケージなら、ヘッダと module map を専用ターゲットの include に置けば、ほかから import WebGPU で参照できます。
C ヘッダがどんな Swift インターフェイスになるかは、Swift 6.2 以降の swift-synthesize-interface ツールで確認できます(Xcode の “Swift 5 interface” でも確認できます)。
xcrun swift-synthesize-interface -I . -module-name WebGPU -target arm64-apple-macos15 -sdk <SDKのパス>
ここからが本題です。ヘッダを書き換えれば Swift への見え方を調整できますが、ライブラリ本体には手を入れたくありません。そこで使うのが Clang の API notes です。module map と同じ場所に、モジュール名に合わせた WebGPU.apinotes という YAML ファイルを置くと、ヘッダを編集せずに同じ情報を外付けできます。以下では「ヘッダを直接書き換える方法」と「API notes で同じことを表現する方法」を対にして見ていきます。最終的にはすべて API notes 側に寄せます。
C の構造を Swift らしく見せる
enum を本物の enum にする
C の enum は、Swift では既定だと「元の整数型を包む struct+グローバル定数」として控えめに import されます。あらゆる用途(選択肢・フラグ集合・単なる定数群)に対応できますが、switch で網羅的に扱えるわけではなく快適ではありません。
選択肢を表す enum なら、Swift の enum にしたいところです。ヘッダを書き換える場合は Clang の enum_extensibility 属性を付けます。API notes なら Tags セクションに次のように書きます。
---
Name: WebGPU
Tags:
- Name: WGPUAdapterType
EnumExtensibility: closed
これで WGPUAdapterType は Swift の enum として import され、短いケース名で switch できるようになります。
switch adapterType {
case .discreteGPU, .integratedGPU:
print("definitely a GPU")
default:
print("not so sure")
}
Tagsは C / C++ の用語で、enum・struct・union・class といった型を指します。これらの型に関する情報はこのセクションに書きます。
参照カウント型を class にする
WebGPU の「オブジェクト」型は、不透明(opaque)なポインタへのエイリアスとして import され、対応する AddRef / Release 関数で手動の参照カウントを行います。これは C と同じく unsafe です。
<swift/bridging> ヘッダの SWIFT_SHARED_REFERENCE を使うと、こうした参照カウント型を 自動参照カウントされる Swift の class に変換できます。API notes では Tags セクションに retain / release 関数を指定します。
- Name: WGPUBindGroupImpl
SwiftImportAs: reference
SwiftReleaseOp: wgpuBindGroupRelease
SwiftRetainOp: wgpuBindGroupAddRef
これで Swift が retain / release を自動管理するため、コード量が減るうえに、解放し忘れ・二重解放といった誤りの可能性がなくなります。
もう一点、参照カウント API では「ある関数が返したオブジェクトを、使い終わったあとに自分で release すべきか」を知る必要があります。WebGPU では、追加で retain 済みのオブジェクトを返す関数(呼び出し側が release 責任を負う)がコメントで示されています。これは SwiftReturnOwnership: retained(ヘッダ側では SWIFT_RETURNS_RETAINED マクロ)で表現でき、Swift が余分な release を自動で補ってくれます。
Functions:
- Name: wgpuDeviceCreateBindGroup
SwiftReturnOwnership: retained
ここまでで、wgpuBindGroupRelease や wgpuBindGroupAddRef を自分で呼ぶ必要は一切なくなります。
関数をメソッド・プロパティ・イニシャライザにする
C の関数は、引数ラベルのないグローバル関数として import されます。SWIFT_NAME(API notes では SwiftName)を使うと、Swift 側の名前と引数ラベルを与えられます。
引数ラベルを付けるだけでなく、第 1 引数が特定の型のインスタンスである関数(C では「その型を操作する関数」)を、その型の メソッド に変換できます。SwiftName の名前を 型名.メソッド名(self:...) の形にし、第 1 引数を self に対応づけます。
- Name: wgpuQueueWriteBuffer
SwiftName: WGPUQueueImpl.writeBuffer(self:buffer:bufferOffset:data:size:)
extension WGPUQueueImpl {
public func writeBuffer(buffer: WGPUBuffer!, bufferOffset: UInt64, data: UnsafeRawPointer!, size: Int)
}
Get 系の関数は、getter: を前置すると 読み取り専用の computed property にできます。
- Name: wgpuQuerySetGetCount
SwiftName: getter:WGPUQuerySetImpl.count(self:)
- Name: wgpuQuerySetGetType
SwiftName: getter:WGPUQuerySetImpl.type(self:)
extension WGPUQuerySetImpl {
public var count: UInt32 { get }
public var type: WGPUQueryType { get }
}
新しいインスタンスを返す関数は、メソッド名を init にすると イニシャライザ になります。SwiftReturnOwnership: retained も併せて指定します。
- Name: wgpuCreateInstance
SwiftReturnOwnership: retained
SwiftName: WGPUInstanceImpl.init(descriptor:)
let instance = WGPUInstance(descriptor: myDescriptor)
メソッド・プロパティ・イニシャライザへの変換は、対象の型が
class(参照型)として import されていることを前提とします。
独自 Bool 型を扱いやすくする
WebGPU は独自の Bool 型 WGPUBool(実体は uint32_t)を定義しているため、そのままだと Swift には UInt32 として入ってきて、true / false も使えません。
Typedefs セクションの SwiftWrapper: struct で、この typedef を専用の struct に包み、UInt32 とは別の独立した型にできます。
- Name: WGPUBool
SwiftWrapper: struct
さらに、Swift 側で少しコードを足して ExpressibleByBooleanLiteral に適合させれば、true / false リテラルで扱えるようになります。型安全性(ほかの整数値と混同できない)と、Bool リテラルの利便性の両方が手に入ります。
extension WGPUBool: ExpressibleByBooleanLiteral {
init(booleanLiteral value: Bool) {
self.init(rawValue: value ? 1 : 0)
}
}
フラグ定数を OptionSet にする
webgpu.h のフラグは、WGPUFlags(64 ビット整数)の typedef とグローバル定数の組で表現されていて、型安全性がありません(別のフラグ群と取り違えても気づけません)。
ここでも SwiftWrapper: struct で専用の struct に包み、グローバル定数を SwiftConformsTo: Swift.OptionSet でその型のメンバーにできます。定数名は Globals セクションの SwiftName で整えられます。
Typedefs:
- Name: WGPUBufferUsage
SwiftWrapper: struct
SwiftConformsTo: Swift.OptionSet
let usageFlags: WGPUBufferUsage = [.mapRead, .mapWrite]
nullability を表現する
WebGPU は WGPU_NULLABLE マクロで「NULL を渡してよいポインタ」を示しています(付いていないポインタは NULL 不可、という含意です)。この情報を Swift に伝えると、implicitly-unwrapped optional(!)を、適切な optional(?)や非 optional に置き換えられます。
API notes では、引数を位置で指定して nullability を与えます。O(_Nullable、optional)/ N(_Nonnull、非 optional)/ U(_Null_unspecified、implicitly-unwrapped optional)から選び、結果型は型と一緒に指定します。
- Name: wgpuCreateInstance
SwiftReturnOwnership: retained
SwiftName: WGPUInstanceImpl.init(descriptor:)
Parameters:
- Position: 0
Nullability: O
ResultType: "WGPUInstance _Nonnull"
extension WGPUInstanceImpl {
public /*not inherited*/ init(descriptor: UnsafePointer<WGPUInstanceDescriptor>?)
}
使い方・注意点
- どの C ライブラリにも応用できます。 まずは本記事のような小さなパッケージを作り、サンプルコードを書きながら Swift への投影を素早く試すのがおすすめです。アノテーションだけで「最良の Swift API」になるとは限りませんが、軽い手間で大半のところまで到達できます。
WGPUBoolをExpressibleByBooleanLiteralに適合させたように、Swift 側で便利な API を足すのも有効です。 - 規則的なヘッダは自動生成と相性が良いです。
webgpu.hは約 6,400 行あり生成物なので、著者は正規表現でヘッダを「パース」してWebGPU.apinotesを生成する小さな Swift スクリプトを書いています。任意の C ヘッダにはより堅牢にlibclangでパースする方法が向きます。多くの C ライブラリは規則的なヘッダを持つため、その規則を利用してまとめてアノテーションを生成できます。 - API notes はヘッダを汚しません。 ヘッダを直接書き換える方法(
enum_extensibility/SWIFT_SHARED_REFERENCE/SWIFT_NAME/ Clang の nullability 属性など)でも同じ結果を得られますが、ライブラリ本体に手を入れずに済む API notes の方が、外部ライブラリには扱いやすい選択肢です。
関連リンク
- Clang Module Maps
- Clang API Notes
- Swift と C++ の相互運用: 共有参照型
- 本記事は C ライブラリを Swift から使う 方向の改善です。逆に Swift の関数や
enumを C へ公開する 方向は SE-0495(C互換関数とenum) を参照してください。