4

I have a function that binds an indirect pointer into a block, and returns the block for later assignment into the direct pointer, like this:

@interface SomeClass : NSObject
@property int anInt;
@end
@implementation SomeClass
@end

typedef void(^CallbackType)(int a);

- (CallbackType)getCallbackToAssignTo:(SomeClass **)indirectPointer {
  return ^(int a){
    NSLog(@"indirectPointer is: %p", indirectPointer);
    NSLog(@"*indirectPointer is: %p", *indirectPointer);
    (*indirectPointer) = [[SomeClass alloc] init];
    (*indirectPointer).anInt = a;
    NSLog(@"After: indirectPointer is: %p", indirectPointer);
    NSLog(@"After: *indirectPointer is: %p", *indirectPointer);
  };
}

- (void)iWillNotDoWhatImSupposedTo {
  SomeClass *directPointer = nil;
  CallbackType cb = [self getCallbackToAssignTo:(&directPointer)];
  NSLog(@"directPointer is pointing to: %p", directPointer);
  NSLog(@"&directPointer is pointing to: %p", &directPointer);
  cb(1);
  NSLog(@"after callback directPointer is: %p", directPointer);
  NSLog(@"after callback &directPointer is: %p", &directPointer);
}

The problem is that while this all compiles and runs, the action of the block is forgotten immediately when the block returns. The printout of running [iWillNotDoWhatImSupposedTo] is:

directPointer is pointing to: 0x0
&directPointer is pointing to: 0x7fff5ce1d060
--- callback execution starts here
indirectPointer is pointing to: 0x7fff5ce1d050
*indirectPointer is pointing to: 0x0
After assignment: indirectPointer is pointing to: 0x7fff5ce1d050
After assignment: *indirectPointer is pointing to: 0x61800001e1d0
--- callback returns here, and the contents of the pointer is lost
after running callback directPointer is pointing to: 0x0
after running callback &directPointer is pointing to: 0x7fff5ce1d060

Any insights for how I can make this callback work?

H G
  • 41
  • 1
  • Not sure how this could be happening, but I think interesting to note that the address where direct pointer is located is different from the address that indirectPointer has (ends in 60, then 50 respectively) – A O Nov 17 '16 at 17:57
  • Presumably in your real code you're passing the address of an ivar to be set? Otherwise you should just make the Block return the new instance directly. – jscs Nov 17 '16 at 18:55

3 Answers3

4

Idali is correct in that it has to do with the fact that the parameter is a "pointer to autoreleasing" and you are passing a "pointer to strong". But it is important to understand why this is relevant here (because in most cases it is not relevant), and it has nothing to do with reference counting per se.

You are passing as the argument a "pointer to strong" (directPointer is a SomeClass * __strong, and thus &directPointer is a SomeClass * __strong *), to a parameter that is "pointer to autoreleasing". How does this work? ARC handles this through a technique called "pass-by-writeback".

Basically, what it does is create a temporary variable of "autoreleasing" type, assign your strong pointer to it (if the value is not null), and then call the method with the pointer to this temporary variable. And then after the method returns, it assigns the value of the variable back out into your strong variable; something approximately like this:

SomeClass * __autoreleasing temporaryPointer = directPointer;
CallbackType cb = [self getCallbackToAssignTo:&temporaryPointer];
directPointer = temporaryPointer;

Note that if the -getCallbackToAssignTo: method only used the argument synchronously within its own execution, there would be no difference between a SomeClass * __strong * parameter and a SomeClass * __autoreleasing * parameter, as the pass-by-writeback mechanism ensures that any changes that have been made during the execution of the method are properly reflected in the strong variable you originally tried to pass a pointer to.

The problem here is that your -getCallbackToAssignTo: method stores the pointer to pointer into a data structure (a block) that outlives the method's execution (the block is returned), which allows some code (the block's body) to later use the stored pointer to try to modify the pointer it points to. But what the pointer points to is the temporary variable that was no longer supposed to be used after the method's execution. So modifying the temporary variable is not reflecting on the original variable.

Even worse, it is basically undefined behavior. The compiler sees that the temporary variable is no longer used after pass-by-writeback scheme, and is potentially allowed to re-use that variable's memory for other things. What you have is a dangling pointer to a variable that the compiler doesn't expect you to still use. And this problem is not really solved by changing the parameter type from "pointer to autoreleasing" to "pointer to strong" -- even though this will get rid of the pass-by-writeback, which may fix this immediate case, the fact remains that the block can be returned out of or stored in a place that can be used outside the scope of the variable you are pointing to, which will still result in you using a dangling pointer. Basically, you have to be very careful when you store or have a block to capture a regular C pointer, because the block has no way of ensuring the lifetime of the variable pointed to (unlike if the block captures an Objective-C object pointer, in which case it retains it).

newacct
  • 119,665
  • 29
  • 163
  • 224
1

I think you must have a strong reference to the newly created object from outside of the block. You could achieve this with a holder class for instance:

@inferface Holder: NSObject
@property (strong) id object;
@end
@implementation Holder
@end

- (CallbackType)getCallbackToAssignTo:(Holder *)inHolder {
    return ^(int a){
        inHolder.object = [[SomeClass alloc] init];
        [inHolder.object setAnInt:a];
    };
}

- (void)thisShouldWork {
    Holder *theHolder = [Holder new];
    CallbackType cb = [self getCallbackToAssignTo:theHolder];

    cb(1);
    SomeClass *directPointer = theHolder.object;
}
clemens
  • 16,716
  • 11
  • 50
  • 65
1

The problem is actually autorelease of the argument. Actually the method signature :

- (CallbackType)getCallbackToAssignTo:(SomeClass **)indirectPointer;

is actually equivalent to:

- (CallbackType)getCallbackToAssignTo:(SomeClass * __autoreleasing *)indirectPointer;

__autoreleasing is used to denote arguments that are passed by reference (id *) and are autoreleased on return.

a solution is to specify __strong as lifetime qualifier.

- (CallbackType)getCallbackToAssignTo:(SomeClass * __strong *)indirectPointer;

more SO reference:Double pointer as Objective-C block parameter

Community
  • 1
  • 1
Idali
  • 1,023
  • 7
  • 10