Skip to content

Commit

Permalink
Fix up some concurrency warnings, deprecate runUnsafely
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Jul 8, 2024
1 parent e4bab59 commit 39504ac
Show file tree
Hide file tree
Showing 8 changed files with 35 additions and 33 deletions.
23 changes: 19 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@ let settings: [SwiftSetting] = [

let package = Package(
name: "MainOffender",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
platforms: [
.macOS(.v10_15),
.macCatalyst(.v13),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
],
products: [
.library(name: "MainOffender", targets: ["MainOffender"]),
],
targets: [
.target(name: "MainOffender", swiftSettings: settings),
.target(name: "MainOffender"),
.testTarget(
name: "MainOffenderTests",
dependencies: ["MainOffender"],
swiftSettings: settings
dependencies: ["MainOffender"]
),
]
)

let swiftSettings: [SwiftSetting] = [
.enableExperimentalFeature("StrictConcurrency"),
]

for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(contentsOf: swiftSettings)
target.swiftSettings = settings
}
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ A tiny package with utilities to help with Swift Concurrency
You can also just copy-paste the stuff you need into your project if you aren't into taking on the dependency. I won't be offended (ha!).

Features:
- `MainActor.runUnsafely` for `MainActor.assumeIsolated` with lower OS version requirements.
- `DispatchQueue.mainActor` for a `DispatchQueue` proxy that is `@MainActor`-compatible
- `OperationQueue.mainActor` for an `OperationQueue` proxy that is `@MainActor` compatible
- `OperationQueue.mainActor` for an `OperationQueue` proxy that is `@MainActor`-compatible
- `UnsafeBlockOperation` for `BlockOperation` without `Sendable` checking
- Additions to `OperationQueue` to submit blocks directly without `Sendable` checking
- `addUnsafeObserver(forName:object:queue:using:)` for `NotificationCenter`
Expand Down
16 changes: 4 additions & 12 deletions Sources/MainOffender/MainActor+RunUnsafely.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import Foundation

public extension MainActor {
extension MainActor {
/// Execute the given body closure on the main actor without enforcing MainActor isolation.
///
/// It will crash if run on any non-main thread.
@available(*, deprecated, message: "Please move to assumeIsolated")
@_unavailableFromAsync
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
#if swift(>=5.9)
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
return try MainActor.assumeIsolated(body)
}
#endif

dispatchPrecondition(condition: .onQueue(.main))
return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
public static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T where T : Sendable{
try MainActor.assumeIsolated(body)
}
}
10 changes: 5 additions & 5 deletions Sources/MainOffender/MainDispatchQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public final class MainDispatchQueue: Sendable {
execute work: @MainActor @escaping @Sendable @convention(block) () -> Void
) {
DispatchQueue.main.async(group: group, qos: qos, flags: flags) {
MainActor.runUnsafely(work)
MainActor.assumeIsolated(work)
}
}

Expand All @@ -21,7 +21,7 @@ public final class MainDispatchQueue: Sendable {
let work = unsafeBitCast(unsafeWork, to: (@Sendable @convention(block) () -> Void).self)

DispatchQueue.main.async(group: group, qos: qos, flags: flags) {
MainActor.runUnsafely(work)
MainActor.assumeIsolated(work)
}
}

Expand All @@ -32,7 +32,7 @@ public final class MainDispatchQueue: Sendable {
execute work: @MainActor @escaping @Sendable @convention(block) () -> Void
) {
DispatchQueue.main.asyncAfter(deadline: deadline, qos: qos, flags: flags) {
MainActor.runUnsafely(work)
MainActor.assumeIsolated(work)
}
}

Expand All @@ -45,7 +45,7 @@ public final class MainDispatchQueue: Sendable {
let work = unsafeBitCast(unsafeWork, to: (@Sendable @convention(block) () -> Void).self)

DispatchQueue.main.asyncAfter(deadline: deadline, qos: qos, flags: flags) {
MainActor.runUnsafely(work)
MainActor.assumeIsolated(work)
}
}
}
Expand All @@ -62,7 +62,7 @@ extension DispatchGroup {
execute work: @MainActor @escaping @Sendable @convention(block) () -> Void
) {
self.notify(qos: qos, flags: flags, queue: .main) {
MainActor.runUnsafely(work)
MainActor.assumeIsolated(work)
}
}
}
8 changes: 4 additions & 4 deletions Sources/MainOffender/MainOperationQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@ import Foundation
public final class MainOperationQueue: Sendable {
public func addOperation(_ block: @MainActor @escaping @Sendable () -> Void) {
OperationQueue.main.addOperation {
MainActor.runUnsafely(block)
MainActor.assumeIsolated(block)
}
}

public func addUnsafeOperation(_ unsafeBlock: @escaping () -> Void) {
OperationQueue.main.addUnsafeOperation {
MainActor.runUnsafely(unsafeBlock)
MainActor.assumeIsolated(unsafeBlock)
}
}

public func addBarrierBlock(_ barrier: @MainActor @escaping @Sendable () -> Void) {
OperationQueue.main.addBarrierBlock {
MainActor.runUnsafely(barrier)
MainActor.assumeIsolated(barrier)
}
}

public func addUnsafeBarrierBlock(_ unsafeBlock: @escaping () -> Void) {
OperationQueue.main.addUnsafeBarrierBlock {
MainActor.runUnsafely(unsafeBlock)
MainActor.assumeIsolated(unsafeBlock)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/MainOffender/UnsafeBlockOperation.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// An operation that manages the concurrent execution of one or more blocks without Sendable checking.
public class UnsafeBlockOperation: BlockOperation {
public class UnsafeBlockOperation: BlockOperation, @unchecked Sendable {
public init(block unsafeBlock: @escaping () -> Void) {
super.init()
addUnsafeExecutionBlock(block: unsafeBlock)
Expand Down
1 change: 1 addition & 0 deletions Tests/MainOffenderTests/MainActorTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import XCTest
import MainOffender

@available(*, deprecated, message: "Here just to validate the legacy API")
final class MainActorTests: XCTestCase {
@MainActor
func testRunOnMainUnsafely() throws {
Expand Down
5 changes: 0 additions & 5 deletions Tests/MainOffenderTests/UnsafeBlockOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,13 @@ final class UnsafeBlockOperationTests: XCTestCase {
}

func testUnsafeBarrierBlock() async throws {
let unsendable = UnsendableClass()

let queue = OperationQueue()
let exp = expectation(description: "completed")

queue.addBarrierBlock {
unsendable.value = 0
exp.fulfill()
}

await fulfillment(of: [exp], timeout: 1.0)

XCTAssertEqual(unsendable.value, 0)
}
}

0 comments on commit 39504ac

Please sign in to comment.