Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

【Swift 6移行】サードパーティのClosureのSendable関連エラーを回避する

iOS開発の福山です。 現在Mirrativ iOSではSwift 6への移行を段階的に行なっています。その中でSwift 6に対応していないサードパーティライブラリに関連する問題にぶつかったため、その回避策を紹介します。

問題

open class Some3rdPartyClass {
    // サードパーティのライブラリなので変更が容易ではない
    open func doSomething(completion: @escaping (Bool) -> Void) { }
}

// -------------------------

final class SomeSubclass: Some3rdPartyClass {
    override func doSomething(completion: @escaping (Bool) -> Void) {
        // error:
        //    Passing non-sendable parameter 'completion' to function expecting a @Sendable closure
        doSomeAction(completion: completion)

        doSomeAction { result in
            // error:
            //   Capture of 'completion' with non-sendable
            //   type '(Bool) -> Void' in a `@Sendable` closure
            completion(result)
        }
    }

    // Sendableなclosureを必要とする何らかの処理
    func doSomeAction(completion: @escaping @Sendable (Bool) -> Void) { ... }
}

ここではSome3rdPartyClassはサードパーティライブラリから提供されているクラスであり、コードの改変が容易ではありません。そのdoSomethingメソッドのcompletion引数に渡されるクロージャは、Sendable(異なるConcurrency domain間で安全に受け渡しできるプロトコル)準拠ではなく、それをoverrideしている箇所でSendableにすることもできません。しかし、doSomeActionメソッドではSendableなクロージャを必要とするため、エラーが発生します。

非SendableなクロージャをSendableなクロージャへ引数として渡す際に以下のようなエラーとなります。

Passing non-sendable parameter 'completion' to function expecting a @Sendable closure

また、非SendableなクロージャをSendableなクロージャの中で使用すると以下のようなエラーとなります

Capture of 'completion' with non-sendable type '(Bool) -> Void' in a `@Sendable` closure

解決策

open class Some3rdPartyClass {
    // サードパーティのライブラリなので変更が容易ではない
    open func doSomething(completion: @escaping (Bool) -> Void) { }
}

// -------------------------

/// サードパーティライブラリからのsubclassやprotocol準拠により
/// Sendableなclosureが作れない場合などに強制的にコンパイラ検証を回避するラッパー
/// 並行処理の安全性を保証する責任は開発者に委ねられる点に注意
private struct UncheckedClosure<T>: @unchecked Sendable {
    typealias Closure = (T) -> Void

    let closure: Closure?

    init(_ closure: Closure?) {
        self.closure = closure
    }
}

final class SomeSubclass: Some3rdPartyClass {
    override func doSomething(completion: @escaping (Bool) -> Void) {
        let uncheckedClosure = UncheckedClosure(completion)
        doSomeAction { result in
            uncheckedClosure.closure?(result)
        }
    }

    // Sendableなclosureを必要とする何らかの処理
    func doSomeAction(completion: @escaping @Sendable (Bool) -> Void) { ... }
}

この方法は、サードパーティの制約に対応するために非Sendableなクロージャをラップし強制的にSendableとして扱う方法です。@unchecked Sendableのstruct UncheckedClosureでラップすることによりコンパイラによるConcurrency検証をスキップできます。並行処理の安全性を保証する責任は開発者に委ねられる点に注意が必要です。

おわりに

サードパーティライブラリがSwift 6対応するまでの一時的な措置の紹介でした。より良い方法や知見がありましたらXまでご連絡いただけると幸いです。

Mirrativ iOSでのSwift 6移行はSwift Package側は完了していますが本体側はまだ道半ばです。コンパイラによるデータ競合チェックから得られる恩恵は大きいと考えているため、チームのメンバーと知見を共有しつつ着実に移行を進めていきたいです。

We are hiring!

ミラティブでは一緒に開発してくれるエンジニアを募集しています!

speakerdeck.com

mirrativ.notion.site

www.mirrativ.co.jp