Swift Digest
Blog | Swift.org Blog

SwiftでのCライブラリの使い勝手を改善する

Improving the usability of C libraries in Swift

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

記事では 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 の流儀がそのまま持ち込まれます。

動きはしますが 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

ここまでで、wgpuBindGroupReleasewgpuBindGroupAddRef を自分で呼ぶ必要は一切なくなります。

関数をメソッド・プロパティ・イニシャライザにする

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>?)
}

使い方・注意点

関連リンク