Swift Digest
SE-0088 | Swift Evolution

Modernize libdispatch for Swift 3 naming conventions

Proposal
SE-0088
Authors
Matt Wright
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

libdispatch は、GCD(Grand Central Dispatch)として知られる並行処理ライブラリで、キューへのタスク投入、グループ化、タイマーやファイル記述子監視などのディスパッチソースといった機能を、低レベルで軽量なインターフェースとして提供します。しかし Swift 2.2 時点での libdispatch は、C の API がほぼそのままインポートされていました。

C 流の関数呼び出しが並ぶ

Swift からは、dispatch_queue_createdispatch_async のようなグローバル関数を、dispatch_queue_t などの不透明型を引き回しながら呼び出す形で使っていました。トレイリングクロージャがあるため最低限は読みやすいものの、全体としては C のコードをそのまま書いているような印象でした。

let queue = dispatch_queue_create("com.test.myqueue", nil)
dispatch_async(queue) {
    print("Hello World")
}

Swift 3 の API デザインガイドラインと噛み合わない

Swift 3 では、SE-0005(Objective-C API のインポート時の名前変換)や SE-0023(API デザインガイドライン)、SE-0006(標準ライブラリへのガイドライン適用)などによって、「型名は大文字キャメルケース、メソッド名は簡潔で動詞ベース、不要な接頭辞を落とす」という方針が整備されました。一方で libdispatch は、型名が dispatch_queue_t のようなスネークケース、操作はすべてグローバル関数、という C 流のスタイルのままで、他の Swift の API とデザインの一貫性を欠いていました。

型安全性が不足している

C の API をそのまま持ち込んでいるため、いくつかの箇所で Swift として不自然な使い方や、誤用しやすい設計が残っていました。

  • dispatch_get_specific / dispatch_queue_get_specific / dispatch_queue_set_specific はキーも値も UnsafePointer<Void> / UnsafeMutablePointer<Void> で扱う必要があり、利用側で毎回キャストを書くことになっていました。型情報が失われているため、誤った型として取り出しても実行時まで気付けません。
  • dispatch_block_t() -> ())に追加のメタデータを付けた「ブロック」を生成する dispatch_block_* 系 API は、戻り値の型が普通のブロックと区別できず、通常のクロージャを渡すべき場所にメタデータ付きブロックを渡す(あるいはその逆の)誤りを型で防げませんでした。
  • dispatch_time_t は実体が UInt64typealias で、DISPATCH_TIME_NOW からの相対指定を作るには dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC))) のように数値のキャストと乗算を手で書く必要があり、Swift としては扱いづらいものでした。
  • DispatchSource は種類ごとに挙動が異なるにもかかわらず、Swift 上では単一の不透明型として見えており、タイマーにしか意味を持たないプロパティをプロセス監視用ソースから触れてしまうなど、誤用の余地がありました。

値型として自然な dispatch_data_t が参照型扱い

libdispatch の dispatch_data_t はもともとイミュータブルなバイト列を表すオブジェクトで、値型として扱うのが自然な設計です。しかし Swift 2.2 では参照型のオブジェクトとしてそのまま公開されており、ArrayString のようにコピーして使う感覚とは揃っていませんでした。

これらを背景に、libdispatch 全体を「Swift 3 の API デザインガイドラインに沿った、オブジェクト指向的で型安全な Swift ライブラリ」として作り直す必要がありました。

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

libdispatch の API 全体を、Swift 3 のネーミング規約とオブジェクト指向的なスタイルに沿って作り直します。Swift 3.0 で実装されています。ランタイムのオーバーヘッドを増やさずに、Swift から見たインターフェースだけを刷新する、という位置づけです。

導入部で示される書き換えのイメージは次のとおりです。

// 従来
let queue = dispatch_queue_create("com.test.myqueue", nil)
dispatch_async(queue) {
    print("Hello World")
}

// Swift 3 以降
let queue = DispatchQueue(label: "com.test.myqueue")
queue.asynchronously {
    print("Hello World")
}

型名を Swift 流に揃える

C の dispatch_*_t 型を、Swift のネーミングに合わせた型名にリネームします。主要な対応は次のとおりです。

C の型 Swift での型
dispatch_object_t DispatchObject
dispatch_queue_t DispatchQueue
dispatch_group_t DispatchGroup
dispatch_data_t DispatchData
dispatch_io_t DispatchIO
dispatch_semaphore_t DispatchSemaphore
dispatch_source_t DispatchSource(サブタイプあり)
dispatch_time_t DispatchTime / DispatchWalltime

また、C では説明用に使われていた dispatch_fd_t(ファイル記述子)や dispatch_block_t(ブロック)、dispatch_queue_attr_t(キューの属性)のような typealias は、Swift 側ではそれぞれ Int32() -> ()DispatchQueueAttributes のように「実体の型」あるいは OptionSet に置き換えられ、意味のない中間型として見えないようにします。

キューの作成とタスク投入をメソッドに寄せる

dispatch_queue_create はイニシャライザに、メインキューとグローバルキューの取得は DispatchQueue のクラスプロパティ/クラスメソッドに置き換わります。

let queue = DispatchQueue(label: "com.test.myqueue")
let main = DispatchQueue.main
let background = DispatchQueue.global(attributes: .qosBackground)

dispatch_sync / dispatch_async / dispatch_group_async のように「キューに対してブロックを投入する」系の関数は、DispatchQueue のメソッドとして synchronously(execute:) / asynchronously(group:qos:flags:execute:) にまとめられます。グループ、QoS、ワークアイテムのフラグはデフォルト引数として asynchronously 側に受け取り、1 つのメソッドで幅広い使い方をカバーします。

queue.asynchronously {
    print("Hello World")
}

queue.asynchronously(group: group, qos: .userInitiated) {
    print("Hello World")
}

queue.synchronously {
    print("Hello World")
}

dispatch_after 相当は after(when:execute:) / after(walltime:execute:)、グループ全体を待つ dispatch_group_waitDispatchGroup.wait(timeout:)、通知用の dispatch_group_notifyDispatchGroup.notify(queue:exeute:) として、それぞれ対応する型のメソッドに移されます。

キュー固有データを型安全にする

dispatch_get_specific 系の API は、DispatchSpecificKey<Value> というジェネリックなキーを介して読み書きする形に置き換わります。キーに値の型情報が紐付くため、キャストなしで安全に取り出せるようになります。

let countKey = DispatchSpecificKey<Int>()
queue.setSpecific(key: countKey, value: 42)

let value: Int? = queue.getSpecific(key: countKey)            // 特定のキューから
let current: Int? = DispatchQueue.getSpecific(key: countKey)  // 現在のキュー階層から

ブロックへのメタデータ付与を専用クラスに切り出す

C の dispatch_block_* 系が作っていた「メタデータ付きブロック」は、DispatchWorkItem という明示的なクラスに置き換わります。通常のクロージャと型で区別できるため、誤って別物を渡してしまう事故を防げます。

let item = DispatchWorkItem(qos: .userInitiated) {
    print("Hello World")
}

queue.asynchronously(execute: item)

item.notify(queue: .main) {
    print("done on main")
}

item.cancel()
if item.isCancelled { /* ... */ }

DispatchWorkItemasynchronously(execute:) / synchronously(execute:) の両方に渡せるため、同じ作業を複数の方法で実行したい場合にも扱いやすくなっています。

時間とインターバルを表す専用の型

dispatch_time_tDispatchTimeDispatchWalltime の 2 つに分かれ、これらに対して足し引きする相対時間として DispatchTimeInterval(秒・ミリ秒・マイクロ秒・ナノ秒の enum)を導入します。Double による秒指定もサポートされるため、小数点付きの秒数でもサブ秒の整数値でも自然に書けます。

enum DispatchTimeInterval {
    case seconds(Int)
    case milliseconds(Int)
    case microseconds(Int)
    case nanoseconds(Int)
}

let a = DispatchTime.now() + 3.5             // 3.5 秒後
let b = DispatchTime.now() + .microseconds(350)

timer.setTimer(start: .now(), interval: .milliseconds(500))

DispatchTime は起動時からのモノトニックな時刻、DispatchWalltime はシステムの実時間(timespec から初期化可能)を表す、という役割分担になっています。

DispatchData を値型にする

dispatch_data_t は、もともとイミュータブルなバイト列を表すオブジェクトでした。これを Swift 上では RandomAccessCollection に適合する struct DispatchData として定義し直し、値型として扱えるようにします。Array<UInt8> のように添字アクセスや for-in でのイテレーションが可能になり、appendsubdata(in:)copyBytes(to:count:) のようなメソッドでデータの組み立てやコピーも自然に書けます。

var data = DispatchData.empty
data.append(buffer)       // UnsafeBufferPointer<UInt8> から追加
data.append(other)        // 別の DispatchData を追加

data.withUnsafeBytes { (ptr: UnsafePointer<UInt8>) in
    // 生バイト列にアクセス
}

data.enumerateBytes { buffer, byteIndex, stop in
    // 連続する領域ごとに走査
}

解放方法を指定する DispatchData.Deallocator として、.freefree を使う)、.unmapmunmap を使う)、.custom(DispatchQueue?, () -> Void)(独自の処理)が用意されています。

ディスパッチソースに型を付ける

C では単一の dispatch_source_t だったディスパッチソースに対して、種類ごとのプロトコルを導入して戻り値の型を強く型付けします。たとえば次のように、ソースの種類別のファクトリメソッドが、種類に対応したプロトコル型を返します。

let timer: DispatchSourceTimer =
    DispatchSource.timer(queue: queue)

let fileObserver: DispatchSourceFileSystemObject =
    DispatchSource.fileSystemObject(
        fileDescriptor: fd,
        eventMask: [.write, .delete],
        queue: queue)

let memoryPressure: DispatchSourceMemoryPressure =
    DispatchSource.memoryPressure(
        eventMask: [.warning, .critical],
        queue: queue)

種類別のプロトコルには次のようなものがあります。

ソースの種類 プロトコル
DISPATCH_SOURCE_TYPE_DATA_ADD DispatchSourceUserDataAdd
DISPATCH_SOURCE_TYPE_DATA_OR DispatchSourceUserDataOr
DISPATCH_SOURCE_TYPE_MACH_SEND DispatchSourceMachSend
DISPATCH_SOURCE_TYPE_MACH_RECV DispatchSourceMachReceive
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE DispatchSourceMemoryPressure
DISPATCH_SOURCE_TYPE_PROC DispatchSourceProcess
DISPATCH_SOURCE_TYPE_READ DispatchSourceRead
DISPATCH_SOURCE_TYPE_SIGNAL DispatchSourceSignal
DISPATCH_SOURCE_TYPE_TIMER DispatchSourceTimer
DISPATCH_SOURCE_TYPE_VNODE DispatchSourceFileSystemObject
DISPATCH_SOURCE_TYPE_WRITE DispatchSourceWrite

また、イベントマスクやプロセスイベントのビットフラグは DispatchSource.ProcessEventDispatchSource.MemoryPressureEventDispatchSource.FileSystemEvent のような OptionSet 型としてネストされ、[.write, .delete] のように集合リテラルで組み立てられます。種類別のプロトコル経由でアクセスできるプロパティも、そのソースに意味のあるものだけに絞られるため、タイマー用のプロパティをプロセス監視ソースから触るといった誤用は型で弾かれます。

既存コードへの影響

libdispatch を使っている Swift コードはすべて影響を受けます。古い dispatch_queue_createdispatch_asyncdispatch_source_createdispatch_data_create などの呼び出しは、新しい DispatchQueue / DispatchGroup / DispatchSource / DispatchData といった型のイニシャライザやメソッドに書き換わります。今日の Swift で当たり前に目にする DispatchQueue.main.asynchronously { ... }DispatchQueue.global(qos: .background)DispatchTime.now() + .milliseconds(500) といった書き方は、この提案による整理の結果として成立しています。