Need help with Swift concurrency and Obj C interoperability

Hello

I wrote a minimal working example which does not compile:

import Foundation

@objc public class Test : NSObject {
var result = false
@objc public func executeAsync() {
Task {
result = true
}
}
@objc public func getResult() -> Bool {
return result
}
}

The error is "Task-isolated value of type '() async -> ()' passed as a strongly
transferred parameter; later accesses could race". All I want to achieve is to
trigger an async method from objective c which generates a result and later
retrieve the result again from objective c. Obviously in the real world code
I would want to have an enum with various states like 'pending', 'success',
'failure' so that the caller can periodically check if the async method has
completed and if yes with what result.

I tried turning the class into an actor, which leads to a different error:
"Actor-isolated instance method '' cannot be @objc"

Thanks for your help!

1 Like

Thankfully, the problem here isn't about Obj-C interoperability, but isolation.

// this type has no isolation
public class Test {
  var result = false
  public func executeAsync() {
    // and so this is mutating unprotected state
    // in the background
    Task {
      result = true
    }
  }
}

How you resolve this kind of depends on how you want to use this type. But, isolating it to the MainActor could address the problem.

The problem is also about Obj-C interoperability because if I just remove all the @objc and turn the class into an actor then it works. This is really about how to correctly deal with isolation AND Obj-C interoperability at the same time. Hopefully somebody will be able to fix my code (WITH @objc) so that it compiles and works.

1 Like

You're right making this an actor does only resolve just the isolation problems. I was super-surprised to learn that you can make actors inherit from NSObject, and that can be handy for some interop problems, so that's something to keep in your back-pocket just in case.

But, here's one option!

import Foundation

@MainActor
@objc public class Test : NSObject {
	var result = false
	@objc public func executeAsync() {
		Task {
			result = true
		}
	}
	@objc public func getResult() -> Bool {
		return result
	}
}

Edit: and sorry I didn't focus in on this right away! You're totally right both parts were important.

Thanks a lot, this compiles indeed! So why does @MainActor work and turning class into an actor doesn't?

Bad news: the code crashes when calling any methods from the objective C side. I found out that @MainActor is the problem. I can create a MWE with nothing but a single empty function, which crashes when @MainActor is present and doesn't crash otherwise.

Therefore the question is still open. Can someone make this work?

Can you share more details about the crash and code that causes is?

Adding MainActor is easy, but does mean this type becomes only safe to use from the main thread. Is it possible this needs to be used from non-main-threads on the Objective-C side?

The crash is a read access to address zero. In the Xcode disassembly I see the crash at a function call and Xcode shows a comment there which says

; symbol stub for: type metadata accessor for Swift.MainActor

which makes it clear that the MainActor concept doesn't work in this kind of situation with interoperability between ObjC and Swift. Online research led me to believe that even the actor keyword compiled in the past in conjunction with @objc and then they made it an error because they found out that it didn't work. I guess they forgot to do the same with @MainActor.

The calling thread is the main thread.

The question is therefore now if it is actually possible to call async Swift functions from ObjC without the actor/MainActor concept. Many Swift libraries have async functions, all those libraries would be unusable for ObjC apps. Our use case is AppTransaction which doesn't exist for ObjC.

I started another thread on the Apple Developer Forum

The suggestion there was to use the async keyword instead of actors.
This works in general, but I still get a crash when the Swift code attempts
to call the completion handler on the objC side.

This result surprised me! So, I made a brand new, ObjC-based macOS test application using Xcode 16, added a Swift class that matches this shape, and then used it from Objective-C like this:

SwiftObj* obj = [SwiftObj new];

NSLog(@"result: %d", obj.getResult);
[obj executeAsync];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
	NSLog(@"result: %d", obj.getResult);
});

It worked, and printed results I would expect. These were both compiled into the same module, could that be a difference?

I wonder if using @preconcurrency to stage the MainActor could help. This does have ABI-implications, and it sounds like that's exactly what's going wrong here. I do not know why though, becuase imports from Swift to ObjC should be aware of these details and handle them automatically.

That would look like this:

@preconcurrency @MainActor
@objc public class Test : NSObject {
    // ...
}

Can you post the Swift code that matches your ObjC code?

Sure! I just changed the type name and put in some logging.

@MainActor
@objc class SwiftObj: NSObject {
	var result = false
	@objc public func executeAsync() {
		print("hello!")
		Task {
			result = true
			print("done!")
		}
	}
	@objc public func getResult() -> Bool {
		return result
	}
}

Tested your code, but in my case the Task is never executed on the Swift side
(the executeAsync() is indeed executed but the embedded task is not).

The other approach, with the async keyword and the objC completion handler actually fully works in my minimal working example, but it doesn't work in the full project yet due to the crash reported in the other discussion. Now I need to focus on the differences between the projects.

Huh. There's definitely something strange going on here.

I do, also, want to make sure you are aware that despite the compiler automating the async <-> completion handler translation, they can have substantially different semantics. It can definitely work, but it often requires much more care than it seems. However, it sounds like you are using the Swift 6 language mode, so I'm pretty sure you are safe on the Swift side.

I was able to resolve the crash that occurred with the async/completion handler method. I had to manually add /usr/lib/swift to the Runpath Search Paths. The small example project automatically added the path as linker flags, my large project didn't do that.

Fantastic! Just make sure you understand the implications of this bridging and how non-isolated async functions behave. They are not identical to Obj-C completion handlers.

In fact, there is a proposal forming to address exactly this:

My completion handler is empty, I still use exactly the same pattern as in my original snippet. I am not too aware of all those subtleties around concurrency, I have actually never coded Swift until a week ago. Apple forces us to use AppTransaction for receipt validation since OS 15 which in turn forces me to use Swift code to do that inside of our C++/ObjC application. Anyway, thanks for your help!