How to do async work in C++ with std::function callback from an ObjC class?

1.8k views Asked by At

Big picture

I have a C++ library that does asynchronous work including networking. It has a Darwin-specific backend that uses the C API of Grand Central Dispatch to delegate work to other threads. Now I'd like to use that library from a new iOS app written in Swift through a thin layer of ObjC++.

I'm using Xcode 6.3.2 on OS X 10.10.


In this minimal example I recreated the architecture described above. The problem is that the ObjC class instance that started an async operation is somehow "broken" when the operation returns through an std::function callback. This happens only if the std::function was declared as [&] instead of [=]. I cannot use the latter though as it is not supported by the "real" C++ code.

The Swift code that calls into ObjC++ looks like this:

class MyViewController : UIViewController {
    var test = ObjectiveTest()

    override func viewDidAppear(animated: Bool) {
        test.testAsyncWork("some data", withHandler: { (result: Int32) -> Void in
            println("Work result: \(result)")
        })
    }
}

The view controller is shown and stays visible if I comment out that code, so it shouldn't kill the ObjectiveTest instance. That is the ObjC++ glue layer:

@interface ObjectiveTest () {
    __block Test *test;
    __block int _a;
}
@end

@implementation ObjectiveTest
- (id)init
{
    self = [super init];
    if (self) {
        test = new Test();
        _a = 42;
        if (!test)
            self = nil;
    }
    return self;
}

- (void)deinit
{
    delete test;
}

- (void)testAsyncWork:(NSString *)someData withHandler:(WorkBlock)handler
{
    _a++;
    NSLog(@"_a = %i", _a); // valid
    NSLog(@"handler = %@", handler); // valid
    test->testAsyncWork(someData.UTF8String, [&](int result) {
        _a++;
        NSLog(@"result = %i", result); // Expected: "result = 666" - valid
        NSLog(@"_a = %i", _a); // Expected: "_a = 44" - invalid
        NSLog(@"handler = %@", handler); // invalid, crashes here
    });
}
@end

Finally, the C++ method that does the "work":

void Test::testAsyncWork(std::string someData, std::function<Handler> handler) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        printf("Data received: %s, calling handler on other thread\n", someData.c_str());
        sleep(1); // This is hard work!

        dispatch_async(dispatch_get_main_queue(), ^{
            printf("Hello, main thread here, calling back now!\n");
            handler(666); // into ObjectiveTest
        });
    });
}

Observations

As I said, this doesn't break if I use [=] for the std::function.

The ivar _a of ObjectiveTest seems to have random values in the callback function despite being declared with __block. The program crashes when trying to access (print/call) the block handler that calls back into Swift code. Xcode displays it like this:

handler WorkBlock & error: summary string parsing error 0xbffa50cc
&handler    __block_literal_generic *   0x7c000000  0x0ae08500
__isa   void *  NULL    0x00000000
__flags int 0   0
__reserved  int 2080876705  2080876705
__FuncPtr   void (*)(int)   0x7c000000  0x7c000000
__descriptor    __block_descriptor *    0x7c085b10  0x7c085b10

From this, I got the impression that the ObjectiveTest instance breaks somewhere in the process, but as it is kept in MyViewController I don't see how this could happen. Did I maybe miss something else?

1

There are 1 answers

1
Yakk - Adam Nevraumont On BEST ANSWER

[&] captures variables by reference. The references captured, if the original variables expire before the task is complete, will be dangling.

As an async call is probably intended to finish asynchronously1, you are basically guaranteed dangling references.

When making an async call, you almost always want to capture by value. You probably even want to list what you are capturing explicitly so you can understand what dependencies you are introducing. (multi threaded code is hard enough without hidden/implicit dependencies)

The only valid use of [&] capture is when you are creating a "visitor" type object to pass into a function which will use the lambda, then discard it and all of its copies, before it returns. Anything besides that you should be capturing by value, or very carefully picking what you capture by reference and proving that the lifetime issues are covered.


1 An example of an async method that might want to [&] is a "run on UI pump thread" async call, where you want to have a worker thread halt progress until the result comes back from the UI thread. There, you'd [&], and block on the returned std::future (or equivalent) before you leave the current scope.