HL666
HL666

Reputation: 105

Send non-sendable closure across isolation domain

I have some weird example related to Swift concurrency:

First, we have this code, and I am using Swift 6

typealias Reply = (Any) -> Void
func callReply1(reply: @escaping Reply) {
  Task {
    reply("hello")
  }
}

In the above code, callReply is a global non-isolated function.

This fails to compile as expected, because reply is not Sendable:

Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure

Then I mark reply to be nonisolated(unsafe):

func callReply2(reply: @escaping Reply) {
  nonisolated(unsafe) let unsafeReply = reply
  Task {
    unsafeReply("hello")
  }
}

I got the same error.

However, weirdly, if I mark the Task as @MainActor, it worked:

func callReply3(reply: @escaping Reply) {
  nonisolated(unsafe) let unsafeReply = reply
  Task { @MainActor in
    unsafeReply("hello")
  }
}

This is very strange - because "non-isolated" means it can be called in any isolation domain.

I wonder why callRely2 fails to compile but callReply3 is fine? Also is it possible to make callReply2 work without using @MainActor?

Note that in real production code, I can't change the reply type to Sendable since it's an API from Objective-C code owned by another team that I am not allowed to change.

Edit: As requested by the comment, here's a more complete reproducible code:

Firstly, I have this objc API that I am not allowed to change. The code looks like this (with some details removed):

// Foo.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

typedef void (^Reply)(id data);
typedef void (^Handler)(Reply reply);

@interface Foo : NSObject
- (id)initWithQueue: (dispatch_queue_t)queue;
- (void)setupHandler: (Handler)handler;
@end
NS_ASSUME_NONNULL_END
// Foo.m
#import "Foo.h"

@implementation Foo {
  dispatch_queue_t _queue;
}
- (id)initWithQueue: (dispatch_queue_t)queue {
  if ((self = [super init])) {
    _queue = queue;
  }
  return self;
}

- (void)setupHandler:(Handler)handler {
  dispatch_async(_queue, ^{
    handler(^(id data){
      NSLog(@"data is %@", data);
    });
  });
}
@end

This code simply calls handler in the specified queue.

Then I have 2 existing async APIs for me to use. Again, not owned by me, so I can't change:


@MainActor
func SomeMainIsolatedAPI() async -> String {
  return "hello"
}

func SomeNonIsolatedAPI() async -> String {
  return "hello"
}

My job is to make sure this works:

func runMainSetup() {
  let foo = Foo(queue: .main)
  foo.setupHandler { reply in
    Task { @MainActor in
      let data = await SomeMainIsolatedAPI()
      reply(data)
    }
  }
}

func runBackgroundSetup() {
  let foo = Foo(queue: .global())
  foo.setupHandler { reply in
    Task {
      let data = await SomeNonIsolatedAPI()
      reply(data)
    }
  }
}

But it fails to compile because reply is not sendable.

The linked question does not answer my question. This is more about asking for a solution, while the other question is more about weird behavior in "@preconcurrency import".

Upvotes: 2

Views: 82

Answers (0)

Related Questions