0

In iOS Programming Book from Big Nerd Ranch (3rd ed) they say on pg.194 ..a knowledgeable programmer could still create an instance of BNRItemStore via allocWithZone:, which would bypass our sneaky alloc trap.To prevent this possibility, override allocWithZone: in BNRItemStore.m to return the single BNRItemStore instance.

+(id) allocWithZone:(NSZone *)zone
{
    return [self sharedStore];
}

This statement seems confusing to me. Doesn't this following code not prove this wrong in a way-

#import <Foundation/Foundation.h>

@interface BNRItemStore : NSObject
+(BNRItemStore *)sharedStore;
+(id)retrieveObject;
@end
@implementation BNRItemStore
+(BNRItemStore *)sharedStore{
    static BNRItemStore *sharedStore=nil;
    if (!sharedStore){
        NSLog(@"Test2");
        sharedStore= [[super allocWithZone:nil] init];
    }
    NSLog(@"sharedStore-> %@",sharedStore);
    return sharedStore;
}
+(id)allocWithZone:(NSZone *)zone{
    NSLog(@"Test1");
    return [self sharedStore];
}
+(id)alloc{
    NSLog(@"Retrieving super object");
    NSLog(@"%@", [super allocWithZone:nil]);//Bypassing the subclass version of    allocWithZone.
    return [super allocWithZone:nil];
}
@end

int main(){
    [[BNRItemStore alloc] init]; //the alloc message triggers a call to the subclass  (overriding) version of +(id)alloc method
}

The output is:

  • 2013-10-18 18:24:40.132 BNRItemStore[381:707] Retrieving super object
  • 2013-10-18 18:24:40.134 BNRItemStore[381:707] BNRItemStore:0x7f8c72c091e0

If the call [super allocWithZone:nil] inside of subclass 'alloc' method would have triggered a call to subclass allocWithZone,the console would be logging "Test1" and "Test2" and finally would lead to static pointer getting allocated. But this did not happen. This means that if we directly call [NSObject allocWithZone:nil] or [super allocWithZone:nil], the message would not redirect to the overriding version (subclass version) of allocWithZone but will give direct access to NSAllocateObject() function which does the actual allocation. The code of +(id)allocWithZone in NSObject must look somewhat like this-

+(id)allocWithZone:(NSZone *)zone{
    return NSAllocateObject();
}

Had this implementation(NSObject's allocWithZone:) included something like [self allocWithZone], the message dispatch mechanism would have included the subclass version of allocWithZone which would then make us go through the "sneaky" trap involving a call to sharedStore method.Following is the case that I'm talking about. Now if this were the case the code would definitely have infinite-looped.Clearly this isn't the case.

+(id)allocWithZone:(NSZone *)zone{
    if([self allocWithZone:zone])      //this would trigger a call to subclass ver. which would call sharedStore method which would then have [super allocWithZone:nil].Infinite Loop
    return NSAllocateObject();
}

So can someone clear up this query about this so called "sneaky" trap. Was the trap meant for blocking anyone from instantiating separately .i.e not being able to use NSObject's allocWithZone except when inside of sharedStore method ? Pls clarify..

BergQuester
  • 6,167
  • 27
  • 39
rahulbsb
  • 115
  • 12

2 Answers2

1

The first, most important lesson here is that you should not override +allocWithZone:. I know the BNR book describes it (and the BNR book is generally very good). You shouldn't do it. I know that Apple includes some example code that does it. You shouldn't do it. (And Apple notes in the explanation that it is rare to need this.) Singletons should be created with the dispatch_once pattern.

You don't give the initial code, but I suspect that their example code overrides alloc, but not allocWithZone:. They're simply saying that if the caller uses allocWithZone:, it won't go through alloc, so they've also overridden alloc to catch that. (Of course the right answer would be just to override allocWithZone: and not alloc. But you shouldn't be overriding these methods in any case.)


EDIT:

I believe you are misunderstanding what "our sneaky alloc trap" means here. The author is assuming the following code at this point in the text:

@interface BNRItemStore : NSObject
+(BNRItemStore *)sharedStore;
@end

@implementation BNRItemStore   
+(BNRItemStore *)sharedStore{
    static BNRItemStore *sharedStore=nil;
    if (!sharedStore){
        sharedStore = [[super allocWithZone:nil] init];
    }
    return sharedStore;
}
@end

That's it; no +alloc overrides at all. It then points out "to enforce the singleton status…you must ensure that another instance of BNRItemStore cannot be allocated." (*)

The author goes on to suggest that we might enforce the singleton status by overriding +alloc, but immediately notes that this is insufficient, since the caller can use +allocWithZone: instead. Since it is documented that [NSObject alloc] calls [self allocWithZone:], it is necessary and sufficient to override +allocWithZone: and unnecessary and insufficient to override +alloc.

What you've done in your code is demonstrate that you can modify BNRItemStore to call [super allocWithZone:] in +alloc. That is not the point. If you can modify BNRItemStore, you could also make it a non-singleton. The point is whether an outside caller (main() in your case) can bypass the singleton instantiation, which she cannot. (**)

(*) The point it doesn't make at this point, and probably should, is that it is generally a bad idea to "enforce the singleton status" by quietly returning a singleton when the callers asked you to allocate a new object. If you need to enforce the singleton status, it is better IMO to do so with an assertion in init, since the request for a second allocation represents a programming error. That said, there are times when "transparent" singletons of immutable objects can be useful for performance reasons, such as the special singletons NSNumber provides for certain common integers, and this technique is appropriate in those cases. (By "transparent," I mean that the singleton-ness is an implementation detail that the caller should never worry about. This presumes at a minimum that the object is immutable.)

(**) Actually she can if she is determined to do so. She could always call NSAllocateObject() herself, bypassing +alloc entirely, and then call -init. This would of course be insane, and there is no reason to "protect" her from herself in doing this. It is not the job of an SDK to protect itself from the caller. It is only the job of an SDK to protect a caller from likely mistakes. The caller is never the enemy.

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thats true. Its unwise to override these methods. But since i have in the above code, could you please tell me if i'm wrong or right about the bypassing subclass version of allocWithZone. I was convinced with the book's code and thought you couldnt escape the trap. But it turns out you can. Is it true or not and if so, is the above the code the way to do so. – rahulbsb Oct 19 '13 at 04:57
  • " The point is whether an outside caller (main() in your case) can bypass the singleton instantiation, which she cannot. (**)" What you mean to say by this is that without meddling with the pre-existing code (the one that contains subclass ver. of allocWithZone implementation), it is enough to have any kind of external function call go through the "trap" which starts from allocWithZone(subclass ver) and ends at sharedStore method(which returns the singleton). – rahulbsb Oct 20 '13 at 11:25
  • Alright then, forget about the alloc implementation in my code (comment it up) and then in the main method [[BNRItemStore alloc] init] runs thru the trap. As we would expect. Now,again, if we change it to [[NSObject alloc] init] then it would not go thru the trap.Same goes for something like this in main method- NSAllocateObject([BNRItemStore class], 10, nil). So isnt that an external exploit from an outside method such as main? Coz this time NSObject version of alloc calls the the NSObject's ver of allocWithZone..So isn't this contrary to the book's point?? – rahulbsb Oct 20 '13 at 11:48
  • You mean if I call `BNRItemStore *x = [[NSObject alloc] init]`? If you do that, then `x` will be of of type `NSObject` and not `BNRItemStore`. Try it yourself and then look at the value of `[x class]`. I explain the `NSAllocateObject()` case in my second footnote. Yes, it is possible for any caller to generate any class of object if they are willing to engage the runtime directly. They could also hand-build an `objc_object` struct with the appropriate `isa` pointer, or bypass the usual message dispatch system. I don't think these advanced cases undermine the basic point being made. – Rob Napier Oct 20 '13 at 18:37
  • None of these should be considered "an external exploit." All of them live within the same program. A program cannot be said to "exploit" itself. – Rob Napier Oct 20 '13 at 18:39
  • Yes you are very right. I tried the case that you talked about. Its true. I got your point 100%. Thanx a lot. This was an important concept that i needed understanding in. – rahulbsb Oct 21 '13 at 08:49
  • There's one more thing i want cleared up. In sharedStore method the singleton object pointer (BNRItemStore type) references [[super allocWithZone:nil] init]. In this case doesnt the BNRItemStore pointer point to NSObject type object given the fact that we used super???? In this case, lets assume we made an external call in the main method- [[BNRItemStore alloc] init]. So the alloc in here shall call subclass allocWithZone which then calls 'sharedStore' inside of which super call is triggered. While using [super allocWithZone] we surely are accessing NSObject's version of allocWithZone. – rahulbsb Oct 21 '13 at 09:11
  • So when we log the BNRItemStore object pointer, why doesn't it show as a pointer pointing to NSObject's object. Pls clarify. – rahulbsb Oct 21 '13 at 09:14
  • `allocWithZone:` sets the class of the object to the originally requested class. Calling `[super alloc]` is not the same thing as calling `[NSObject alloc]`. Using `super` does not change `self` in the class method. It even dispatches with a different function, objc_msgSendSuper rather than objc_msgSend. – Rob Napier Oct 21 '13 at 12:14
0

i'm not sure if this quite answers your question or not, but "allocWithZone:" was used back in the day to be able to partition the memory allocated. apple has since moved away from this concept and expects everything to be allocated in the same heap space. "allocWithZone:" does not even function the way it used to, and apple specifically says not to use it.

DoS
  • 1,454
  • 13
  • 18
  • Yes that definitely is true but since i'm in the learning phase right now and following the BNR book chapter by chapter, i'm gonna stay on this track. – rahulbsb Oct 19 '13 at 04:55