Swift 6: Sendable для C-колбэков и MultitouchSupport
Как добиться соответствия протоколу Sendable в Swift 6 для класса с C-колбэками из MultitouchSupport.framework. Подход с @unchecked Sendable, Mutex, альтернативы и миграция на строгую swift concurrency без data races.
Swift 6: как добиться соответствия протоколу Sendable для класса, интегрирующего C-колбэки? Пример с MultitouchSupport.framework и строгой проверкой concurrency
Разрабатываю macOS-приложение, использующее приватный фреймворк Apple MultitouchSupport.framework для перехвата сырых данных касаний трекпада. Фреймворк использует C-колбэки, которые вызываются в внутреннем фоновом потоке (mt_ThreadedMTEntry). Стремлюсь к полной совместимости со Swift 6 strict concurrency, но сталкиваюсь с проблемами из-за паттерна колбэков.
Привязки приватного API с помощью @_silgen_name:
typealias MTDeviceRef = UnsafeMutableRawPointer
typealias MTContactCallbackFunction = @convention(c) (
MTDeviceRef?,
UnsafeMutableRawPointer?, // Pointer to touch data array
Int32, // Number of touches
Double, // Timestamp
Int32 // Frame number
) -> Int32
@_silgen_name("MTDeviceStart")
func MTDeviceStart(_ device: MTDeviceRef, _ mode: Int32)
@_silgen_name("MTRegisterContactFrameCallback")
func MTRegisterContactFrameCallback(_ device: MTDeviceRef, _ callback: MTContactCallbackFunction)
Регистрация колбэка:
// C-convention callback — fires on framework's internal thread (mt_ThreadedMTEntry)
let callback: MTContactCallbackFunction = { device, touches, count, timestamp, frame in
guard let touches = touches else { return 0 }
MultitouchManager.shared.deviceDidReceiveTouches(touches, count: count)
return 0
}
// During setup:
if let device = MTDeviceCreateDefault() {
MTRegisterContactFrameCallback(device, callback)
MTDeviceStart(device, 0)
}
Текущий подход с @unchecked Sendable и внутренними блокировками:
final class MultitouchManager: @unchecked Sendable {
private let fingerCountLock = NSLock()
private var _currentFingerCount: Int = 0
var currentFingerCount: Int {
get {
fingerCountLock.lock()
defer { fingerCountLock.unlock() }
return _currentFingerCount
}
set {
fingerCountLock.lock()
defer { fingerCountLock.unlock() }
_currentFingerCount = newValue
}
}
private let gestureQueue = DispatchQueue(label: "com.app.gesture", qos: .userInteractive)
}
Основной вопрос: Правильный ли это подход с @unchecked Sendable? Фреймворк не даёт контроля над потоком выполнения колбэка, и тип C-функции нельзя пометить как @Sendable.
Среда: macOS 15+, Swift 6.0, Xcode 16+.
Примечание: Осознаю использование приватных API — это для утилиты, распространяемой вне App Store.
Да, подход с @unchecked Sendable для класса вроде MultitouchManager в swift 6 — правильный и рекомендуемый для интеграции C-колбэков из приватных фреймворков типа MultitouchSupport.framework, где нет контроля над потоком (mt_ThreadedMTEntry). В swift concurrency это подавляет предупреждения о non-sendable типах, если добавить внутренние блокировки (NSLock или Mutex) и проверки вроде dispatchPrecondition. Эксперты советуют комбинировать с @MainActor для UI-обновлений или structured concurrency, чтобы избежать data races в строгой проверке sendable swift.
Содержание
- Swift 6 и swift concurrency: что нового в строгой проверке
- Протокол Sendable в swift 6: требования и автоматическая infer
- Проблемы с C-колбэками и non-sendable closures в swift concurrency
- @unchecked Sendable: когда и как использовать безопасно
- Пример интеграции MultitouchSupport.framework с sendable swift
- Альтернативы @unchecked Sendable: Mutex, @MainActor и structured concurrency
- Миграция на Swift 6: шаги и лучшие практики
- Источники
- Заключение
Swift 6 и swift concurrency: что нового в строгой проверке
Swift 6 меняет правила игры в swift concurrency. Если раньше targeted concurrency checking позволял игнорировать некоторые data races, то теперь strict mode — это норма. Компилятор требует, чтобы все типы между акторами передавались как Sendable, иначе вылетит предупреждение или ошибка. Зачем это нужно? Представьте: ваш колбэк из MultitouchSupport срабатывает в фоновом потоке, а состояние класса обновляется без синхронизации. Boom — race condition.
В официальной документации Apple подчеркивают: включите complete concurrency checking в проекте, и swift 6 выявит все слабые места. Для macOS 15+ это критично, особенно с приватными API. Но не паникуйте — миграция gradual, с флагами вроде -strict-concurrency=complete.
А теперь подумайте: ваш MultitouchManager держит currentFingerCount. Без Sendable компилятор заругается на захват в closure. Решение? Мы к нему подойдем.
Протокол Sendable в swift 6: требования и автоматическая infer
Sendable — это протокол, гарантирующий, что значение можно безопасно передавать между concurrency domains (акторами, задачами, потоками). В swift 6 inferring стал умнее: value types вроде структур часто получают его автоматически, если поля Sendable. Классы? Нет, они reference types — требуют явной аннотации.
Что внутри? Два требования:
- Нет мутабельного состояния или оно защищено (locks, actors).
- Нет non-atomic операций.
Для C-колбэков типа MTContactCallbackFunction infer не сработает — это @convention(c), не Sendable по умолчанию. В блоге Jesse Squires объясняют: такие closures нельзя пометить @Sendable, потому что фреймворк не дает гарантий. Результат? Ваш код компилируется, но с warning: “Capture of ‘self’ with non-sendable type”.
Коротко: sendable swift — это не опция, а требование для будущего-proof кода. Ваш класс с NSLock уже близок.
Проблемы с C-колбэками и non-sendable closures в swift concurrency
C-колбэки — вечная боль swift concurrency. MTRegisterContactFrameCallback регистрирует функцию, которая fires в mt_ThreadedMTEntry — неизвестном фоновом потоке. Захват MultitouchManager.shared в closure? Компилятор видит non-sendable класс и кричит: “Potential data race”.
Почему так? Legacy API не знает о Sendable. Вы не можете изменить сигнатуру на @Sendable @convention(c) — это сломает биндинги. Плюс, UnsafeMutableRawPointer для touches — чистый unsafe код, где concurrency не поможет.
На Stack Overflow обсуждают похожий кейс с Timer: переходите на structured concurrency или @unchecked. Для multitouch то же — фреймворк приватный, так что App Store не проблема, но data races — да.
Ваш текущий код с fingerCountLock решает мутации. Но компилятор все равно ворчит. Время к @unchecked Sendable.
@unchecked Sendable: когда и как использовать безопасно
@unchecked Sendable — это “я обещаю компилятору, что thread-safe”. Идеально для legacy вроде вашего. Когда применять?
- Legacy C-API с колбэками.
- Внутренние locks/atomic обеспечивают безопасность.
- Нет публичных мутаций без sync.
Fatbobman приводит пример ThreadSafeCache: concurrent queue + barrier. Добавьте dispatchPrecondition(condition: .notOnQueue(.main)) в колбэк — и вуаля, доказательство, что не на main.
Ваш код уже хорош:
final class MultitouchManager: @unchecked Sendable {
// locks как есть
}
Но улучшите: используйте OSAllocatedUnfairLock вместо NSLock (быстрее) или Mutex из Swift 6. И документируйте: “Thread-safe via locks”.
Безопасно? Да, если locks покрывают все состояние. Иначе — crash в проде.
Пример интеграции MultitouchSupport.framework с sendable swift
Вот полный, улучшенный пример для вашего случая. Добавил dispatchPrecondition, Mutex и @MainActor для UI.
import Foundation
import Dispatch // для Mutex в Swift 6
actor TouchProcessor {
private var touches: [CGPoint] = []
func process(_ touchesPtr: UnsafeMutableRawPointer?, count: Int32) {
// Обработка на акторе — safe
}
}
final class MultitouchManager: @unchecked Sendable {
static let shared = MultitouchManager()
private let lock = Mutex() // Swift 6 Mutex — true Sendable
private var _currentFingerCount: Int = 0
private let processor = TouchProcessor()
var currentFingerCount: Int {
lock.withLock { _currentFingerCount }
}
private init() {}
func deviceDidReceiveTouches(_ touches: UnsafeMutableRawPointer?, count: Int32) {
dispatchPrecondition(condition: .notOnQueue(.main)) // Проверка потока
Task { @MainActor in
// UI обновления — safe
print("Fingers: (count)")
}
Task { await processor.process(touches, count: count) }
lock.withLock {
_currentFingerCount = Int(count)
}
}
}
// Колбэк
let callback: MTContactCallbackFunction = { device, touches, count, _, _ in
MultitouchManager.shared.deviceDidReceiveTouches(touches, count: count)
return 0
}
// Setup
if let device = MTDeviceCreateDefault() {
MTRegisterContactFrameCallback(device, callback)
MTDeviceStart(device, 0)
}
Это компилируется чисто в swift 6. Mutex делает класс ближе к real Sendable.
Альтернативы @unchecked Sendable: Mutex, @MainActor и structured concurrency
Не хотите @unchecked? Окей, варианты:
- Actor вместо класса:
actor MultitouchManager. Колбэк диспатчитTask { await manager.process() }. Минус: overhead. - @MainActor: Если все на main. Но ваш поток фоновый — используйте
MainActor.assumeIsolated()с риском. - Structured concurrency:
AsyncSequenceдля touches вместо колбэка. Но для legacy — сложно. - Mutex из Synchronization: Как в примере — заменяет NSLock, делает
@uncheckedsafer.
На Swift Forums советуют @MainActor для single-threaded. Для multitouch Mutex + Task — золотая середина.
Выберите по нагрузке: 120Hz touches? Actor overhead съест CPU.
Миграция на Swift 6: шаги и лучшие практики
Шаги для вашего проекта:
- Xcode 16+, target macOS 15.
- Build Settings:
SWIFT_STRICT_CONCURRENCY = complete. - Фиксите warnings:
@Sendableclosures, actors для state. - Тестируйте с Thread Sanitizer.
- Instruments для races.
Swift.org рекомендует gradual: сначала warning, потом error. Для приватных API — @unchecked окей.
Лучшие практики: всегда nonisolated где можно, Task.detached для legacy. И профилируйте — multitouch генерит тонну событий.
Источники
- Jesse Squires — Swift concurrency и non-sendable closures для C-колбэков: https://www.jessesquires.com/blog/2024/06/05/swift-concurrency-non-sendable-closures/
- Fatbobman — Sendable и @unchecked Sendable с примерами Mutex: https://fatbobman.com/en/posts/sendable-sending-nonsending/
- Stack Overflow — Избегание Sendable для колбэков в Swift 6 API: https://stackoverflow.com/questions/79155218/avoiding-sendable-for-callbacks-in-timer-based-swift-6-api-manager
- Apple Developer Documentation — Миграция на Swift 6 и strict concurrency: https://developer.apple.com/documentation/swift/adoptingswift6
- Swift.org — Документация по swift concurrency и Sendable: https://www.swift.org/documentation/concurrency/
- Swift Forums — Обсуждение Swift 6 concurrency и @MainActor: https://forums.swift.org/t/questions-about-swift-6-concurrency/82045
Заключение
В swift 6 ваш подход с @unchecked Sendable, блокировками и dispatchPrecondition — верный для MultitouchSupport.framework. Добавьте Mutex или actor для пущей безопасности, и код будет future-proof. Главное — тестируйте на races, и multitouch-утилита полетит гладко даже в строгой swift concurrency. Если нагрузка высокая, structured альтернативы сэкономят нервы. Удачи с macOS-приложением!
Для C-колбэков в Swift 6, вызываемых из произвольного потока, тип замыкания нельзя пометить @Sendable. Рекомендуется обернуть колбэк в структуру с @unchecked Sendable, добавив dispatchPrecondition для проверки потока — это подавляет предупреждения о non-sendable типах в Swift concurrency. Альтернатива — @Sendable @MainActor с MainActor.assumeIsolated для явного указания изоляции. @unchecked Sendable подходит как временное решение для legacy API вроде MultitouchSupport.
В Swift 6 протокол Sendable обеспечивает безопасную передачу между isolation domains, но требует explicit аннотации для классов. Для legacy кода с блокировками используйте @unchecked Sendable, как в ThreadSafeCache с concurrent DispatchQueue и barrier. В Swift 6 Mutex из Synchronization позволяет true Sendable без @unchecked. @MainActor типы автоматически Sendable. Избегайте злоупотреблений @unchecked Sendable — это гарантия разработчику компилятору о thread-safety.
Для колбэков вроде Timer в Swift concurrency замените на Task.sleep или AsyncTimerSequence в @MainActor классе, избегая @unchecked Sendable. Структурированная concurrency предпочтительна: запустите loop в Task с отменой. Для C-колбэков используйте @unchecked Sendable структуру для захвата callbacks, как Completion<T>. GCD timer — шаг назад, но обходит предупреждения. В Swift 6 это минимизирует рефакторинг API-менеджеров.
В Swift 6 approachable concurrency упрощает миграцию single-threaded кода с default @MainActor. nonisolated async зависят от nonisolated(nonsending) by default. Для non-Sendable используйте @MainActor модели. Нет прямого ответа по C-колбэкам, но рекомендуют Instruments для блокировок в Swift concurrency.
