A memory leak when capturing callback function from block of setTerminationHandler of NSTask

278 Views Asked by At

I created a simple code and found there is a memory leak:

#import <Foundation/Foundation.h>
#include <functional>

void testLeaks(const std::function <void (int)>& inCallback) {
    NSTask* task = [[NSTask alloc] init];
    [task setTerminationHandler:^(NSTask* inTask) {
        inCallback(inTask.terminationStatus);
    }];

  [task release];
}
int main(int argc, const char * argv[]) {
  auto callback = [](int result) {
    NSLog(@"callback");
  };
  testLeaks(callback);
  NSLog(@"done");
  return 0;
}

The output of leaks:

Process:         testLeaks2 [13084]
Path:            /    Users/USER/Library/Developer/Xcode/DerivedData/Build/Products/Debug/testLeaks2
Load Address:    0x10a04d000
Identifier:      testLeaks2
Version:         ???
Code Type:       X86-64
Platform:        macOS
Parent Process:  leaks [13083]

Date/Time:       2022-02-28 23:34:28.374 +0800
Launch Time:     2022-02-28 23:34:27.939 +0800
OS Version:      Mac OS X 10.15.6 (19G73)
Report Version:  7
Analysis Tool:   /Applications/Xcode.app/Contents/Developer/usr/bin/leaks
Analysis Tool Version:  Xcode 12.4 (12D4e)
----

leaks Report Version: 4.0
Process 13084: 596 nodes malloced for 59 KB
Process 13084: 1 leak for 48 total leaked bytes.

    1 (48 bytes) ROOT LEAK: <__NSMallocBlock__ 0x7fbbc2704350> [48]  testLeaks2  invocation function for block in testLeaks(std::__1::function<void (int)> const&)  0..."

Looks the issue is related to std::function in the block of setTerminationHandler. How do I address this memory leak issue if I want to keep using std::function as a callback? Thanks a lot!

1

There are 1 best solutions below

0
Carl Colijn On

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)

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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.