I ran into something similar today on Xcode 12.3. In my case I had the following function, feeding it C++ lambdas as completion handlers:
id<MTLCommandBuffer> MetalGPU::GetCommandBuffer(const std::function<void(id<MTLCommandBuffer> commandBuffer)>& CompletionHandler) {
id<MTLCommandBuffer> commandBuffer = [m_commands commandBuffer];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> finalCommandBuffer) {
CallContext context;
RunCodeInNewCallstack(context, [&finalCommandBuffer, CompletionHandler]() {
CompletionHandler(finalCommandBuffer);
});
}];
return commandBuffer;
}
So I took your idea and one-upped it by adding an extra innermost C++ lambda to the mix :) On my system this makes the program blow up at runtime when the std::function tries to copy itself into that innermost C++ lambda;
Thread 3: EXC_BAD_ACCESS (code=2, address=0x7fff872ebd38)
0x000000010d9dabbc in std::__1::__function::__value_func<void (id<MTLCommandBuffer>)>::__value_func(void const(&)(id<MTLCommandBuffer>)) at /Applications/Xcode12.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/functional:1808
0x000000010d9dab1d in std::__1::__function::__value_func<void (id<MTLCommandBuffer>)>::__value_func(void const(&)(id<MTLCommandBuffer>)) at /Applications/Xcode12.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/functional:1799
0x000000010d9daaed in std::__1::function<void (id<MTLCommandBuffer>)>::function(void const(&)(id<MTLCommandBuffer>)) at /Applications/Xcode12.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/functional:2468
0x000000010d9d67dd in std::__1::function<void (id<MTLCommandBuffer>)>::function(void const(&)(id<MTLCommandBuffer>)) at /Applications/Xcode12.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/functional:2468
0x000000010d9d6713 in invocation function for block in MetalGPU::GetCommandBuffer(std::__1::function<void (id<MTLCommandBuffer>)> const&) at <my file path>, resolving to the line RunCodeInNewCallstack(context, [&finalCommandBuffer, CompletionHandler]() {
After experimenting with some possible solutions I also observed some related RAII-managed bookkeeping to be decremented below 0, so there's definitely something fishy going on with object lifetimes.
It seems that Xcode doesn't make a local (deep) copy of the passed in std::function object to be used in the Objective-C block? It probably only makes a copy of the pointer (since it's passed by ref), and when the original goes out of scope and dies we're left with garbage. Making a local copy of the passed-in std::function outside the block (by adding auto CompletionHandlerCopy = CompletionHandler) and using that in the block indeed makes the problem go away.
So my proposed solution is: make it easy on yourself, and don't pass the std::function into your function by const ref but by value instead, so that that needed copy is already made for you by the compiler. I changed mine to:
id<MTLCommandBuffer> MetalGPU::GetCommandBuffer(std::function<void(id<MTLCommandBuffer> commandBuffer)> CompletionHandler) {
// Note: we get passed the CompletionHandler by value. When passing it by const
// ref, accessing it in the C++ lambda in the Objective-C block below blows up.
// Xcode in that case doesn't make a true copy of the CompletionHandler object for
// the Objective-C block (the `^(){}` part), probably because it's passed by ref so
// it makes a copy of the pointer. The original then goes out of scope and gets
// destroyed before the inner lambda can use it.
id<MTLCommandBuffer> commandBuffer = [m_commands commandBuffer];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> finalCommandBuffer) {
C_CallContext context;
RunCodeInNewCallstack(context, [&finalCommandBuffer, &CompletionHandler]() {
CompletionHandler(finalCommandBuffer);
});
}];
return commandBuffer;
}
I don't know if this will help with your memory leak problem though, but it did help with my crash.