1

How to resolve error:

Non-Sendable parameter type BookInfo of actor-isolated @objc instance method cannot cross actor boundary?

BookStore.swift:

// manage state of instances of BookInfo

@objc actor BookStore: NSObject {
@objc static let shared = BookStore()
private var bookCache = [String: BookInfo]()

@objc func registerBook(bookInfo: BookInfo) async { //error: Non-Sendable parameter type `BookInfo` of actor-isolated @objc instance method cannot cross actor boundary
    bookCache[bookInfo.id] = bookInfo
}

@objc func updateBookInfo(id: String) async {
    // update properties in BookInfo
}

ClassA.m:

@implementation ClassA {
- (void)register() {
    BookInfo someBookInfo = [[BookInfo alloc] initWithID:id author: author];
    [[BookStore shared] registerBook:someBookInfo completionHandler^(){}];
    // After rigistering book, it will not need read/write `someBookInfo` anymore
}

BookInfo.h:

@interface BookInfo : NSObject

@property (strong) Report *report;
@proptery (assign) NSString *id;
@property (Strong) Author: *author;

- (instancetype) initWithID:(NSString *)id author:(Author*)author;
// ...
//some method

@end

Discussion: Since the caller will not read/write someBookInfo after initialize it and pass in it as a param in registerBook, it is guaranteed that there will not be race condition on someBookInfo between the thread which runs the caller's method and the thread runs the calling method. Adding extension BookInfo: @unchecked Sendable {} will resolve the error at compiler time. I want to avoid that for certain risks that I'm not aware of. Any discussion/advice will be appreciated.

1
  • If you don't want to mark BookInfo as unchecked Sendable, then do not send a BookInfo class instance across actor boundaries. Commented Oct 31 at 20:54

1 Answer 1

0

Since the caller will not read/write someBookInfo after initialize it and pass in it as a param in registerBook, it is guaranteed that there will not be race condition on someBookInfo between the thread which runs the caller's method and the thread runs the calling method.

Your analysis is correct, but the Objective-C compiler does not do this analysis. This is not one of its features. The Swift compiler knows this, and so does not allow Objective-C code from sending in non-sendable things, erring on the side of caution.

You can add an additional overload that takes a & Sendable to force the actor method to compile,

// call this in your Objective-C code
@objc func unsafeRegisterBook(bookInfo: any BookInfo & Sendable) async {
    await registerBook(bookInfo: bookInfo)
}

func registerBook(bookInfo: BookInfo) async {
    bookCache[bookInfo.id] = bookInfo
}

Just as @unchecked Sendable, it will rely on your own due diligence to check that you are indeed passing the parameter safely. This is at least better than making the whole type @unchecked Sendable, since the "unsafeness" is only limited to this particular method.


That said, I still recommend making a sendable version of BookInfo, containing all the information needed to construct a BookInfo, and also provide a method for converting to an actual BookInfo.

@objc
@objcMembers
final class SendableBookInfo: NSObject, Sendable {
    let id: String
    // all the properties should be 'let'...

    init(_ bookInfo: BookInfo) { ... }

    var bookInfo: BookInfo { ... }
}

then use this type as the parameter type.


P.S. The Swift compiler does do this analysis, as part of its "region-based isolation" feature.

let book = BookInfo()
await BookStore.shared.registerBook(bookInfo: book)
print(book)

This produces the error "Sending 'book' risks causing data races", and removing the line print(book) also removes the error.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.