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?
[&]
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 returnedstd::future
(or equivalent) before you leave the current scope.