5

I have a problem that seems to be a premature release of an in-use object in an ARC based app. I'm trying to create a folder on an FTP server. The relevant parts of code are below; i'll describe the problem first.

problem with the code is, that the debug output in the

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode

method is never called.

Instead, i just get an _EXC_BAD_ACCESS_ error. While debugging i found out two things:

  1. the error only appears if the following line of code (createDir method) is executed:

    [ftpStream open];
    

if that message isn't sent, the rest of the code doesn't really make sense - but it doesn't crash either...

  1. I tracked the EXC_BAD_ACCESS down with NSZombieEnabled: With zombie objects enabled, the GDB produces the following debugger info:

     *** -[FTPUploads respondsToSelector:]: message sent to deallocated instance 0x9166590
    

The referred address 0x9166590 is the address of my FTPUploads object. It looks like the streams delegate is deallocated before it can handle messages.

Why does the system deallocate an in-use object? How can i prevent it from being deallocated prematurely?

code:

FTPUploads.h excerpt:

#import <Foundation/Foundation.h>

enum UploadMode {

    UploadModeCreateDir, 
    UploadModeUploadeData
};

@class UploadDatasetVC;

@interface FTPUploads : NSObject<NSStreamDelegate> {

    @private
    NSString *uploadDir;
    NSString *ftpUser;
    NSString *ftpPass;

    NSString *datasetDir;
    NSArray *files;

    /* FTP Upload fields */
    NSInputStream *fileStream;
    NSOutputStream *ftpStream;
    // some more fields...
    enum UploadMode uploadMode;
    UploadDatasetVC *callback;
}

- (id) initWithTimeseriesID: (int) aTimeseriesID 
             fromDatasetDir: (NSString *) aDir
                  withFiles: (NSArray *) filesArg 
          andCallbackObject: (UploadDatasetVC *) aCallback;

- (void) createDir;

@end

FTPUploads.m excerpt

#import "FTPUploads.h"
#import "UploadDatasetVC"

@implementation FTPUploads

- (id) initWithTimeseriesID: (int) aTimeseriesID 
             fromDatasetDir: (NSString *) aDir
                  withFiles: (NSArray *) filesArg 
          andCallbackObject: (UploadDatasetVC *) aCallback {

    self = [super init];

    if (self) {

        uploadDir = [NSString stringWithFormat: @"ftp://aServer.org/%i/", aTimeseriesID];
        ftpUser = @"aUser";
        ftpPass = @"aPass";

            datasetDir = aDir;
            files = filesArg;

        bufferOffset = 0;
        bufferLimit = 0;

        index = 0;

        callback = aCallback;
    }

    return self;
}

- (void) createDir {

    uploadMode = UploadModeCreateDir;
    NSURL *destinationDirURL = [NSURL URLWithString: uploadDir];

    CFWriteStreamRef writeStreamRef = CFWriteStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) destinationDirURL);
    assert(writeStreamRef != NULL);

    ftpStream = (__bridge_transfer NSOutputStream *) writeStreamRef;
    [ftpStream setProperty: ftpUser forKey: (id)kCFStreamPropertyFTPUserName];
    [ftpStream setProperty: ftpPass forKey: (id)kCFStreamPropertyFTPPassword];

    ftpStream.delegate = self;
    [ftpStream scheduleInRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];
    // open stream
    [ftpStream open];

    CFRelease(writeStreamRef);
}

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {

    NSLog(@"aStream has an event: %i", eventCode);

    switch (eventCode) {
        // all cases handled properly
        default:
            // no event
            NSLog(@"default mode; no event");
            break;
    }
}

EDIT: added creation code that is used in the class UploadDatasetVC:

FTPUploads *uploads = [[FTPUploads alloc] initWithTimeseriesID: timeseries_id 
                                                fromDatasetDir: datasetDir 
                                                     withFiles: files 
                                             andCallbackObject: self];
[uploads createDir];
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
m Jae
  • 725
  • 2
  • 8
  • 20
  • Objects don't typically retain (or in the case of arc, retain strong references to) their delegates. Seems to me like the problem will be with the code that creates your FtpUploads object and what it subsequently does with it – Frederick Cheung Feb 05 '12 at 15:23
  • i'm adding that code in another snippet of the question. also see my comment on the first answer. – m Jae Feb 05 '12 at 15:49
  • What then happens to it? If uploads is just a local variable the when it goes out of scope ARC will zap it – Frederick Cheung Feb 05 '12 at 15:53
  • yes, you are completely right; see @grahamparks answer below for what i did to get around this issue. making it a class variable wasn't sufficient, so i created a property for the uploads. – m Jae Feb 05 '12 at 16:03

3 Answers3

3

It looks to me like the only reference to your FTPUploads object is the delegate property on the stream. This won't retain your object, so if nothing else has a reference to the object, the object will be dealloced. A.R.C. doesn't try to prevent this scenario.

What you need to do is have the code that allocates the FTPUploads object keep a reference to the object until it completes.

It also wouldn't be a bad idea to set the ftpStream.delegate property to nil in your FTPUploads dealloc method, as this will prevent a crash if the object is dealloced prematurely.

grahamparks
  • 16,130
  • 5
  • 49
  • 43
  • no; the calling code for creating the FTPUploads object holds the first reference to that object? in the 'createDir' method of i set 'self' as an delegate - ok. but first reference is created in UploadDatasetVC, where i create hte FTPUploads object: 'FTPUploads *uploads = [[FTPUploads alloc] initWithTimeseriesID: timeseries_id fromDatasetDir: datasetDir withFiles: files andCallbackObject: self]; [uploads createDir];' – m Jae Feb 05 '12 at 15:47
  • Most delegate properties are set as "weak" or "assign", which means they don't count towards the retain count. Your `uploads` variable will only count if it continues to exist for as long as the connection does, which I'm guessing it doesn't. – grahamparks Feb 05 '12 at 15:50
  • 1
    ok, one workaround is to save the used FTPUploads object as a property with strong reference. this is what works best for me; adding the following line to UploadDataset.h: '@property (strong) FTPUploads *uploads;' and adapt the object creation in UploadDataset.h accordingly – m Jae Feb 05 '12 at 16:01
2

The problem is that your ftpStream object is being deallocated. You create it with CFWriteStreamCreateWithFTPURL(), then release it with CFRelease(). You used a __bridge cast, which basically means "don't do any memory management on this assignment". So ARC didn't retain it when you assigned it to ftpStream. Since your intention was to transfer ownership from CF to ARC, that was the wrong cast to use.

You actually wanted either __bridge_retained or __bridge_transfer. I can never remember which is which, though. Luckily, there's another option—the CFBridgingRetain() and CFBridgingRelease() macros. They resolve down to those same bridging casts, but are named far more clearly.

In this case, you want CF to release it, but bridge it over to ARC. So you want CFBridgingRelease(). That will tell ARC to take ownership of the object, and then do a CFRelease. In short, replace this:

ftpStream = (__bridge NSOutputStream *) writeStreamRef;

with this:

ftpStream = CFBridgingRelease(writeStreamRef);

And then remove the call to CFRelease() a few lines later.

BJ Homer
  • 48,806
  • 11
  • 116
  • 129
  • wrong. as stated in my original question the problem is the FTPUploads object being deallocated - not the ftpStream! that's just another bug in my code but not the original problem. thanks for the hint thought – m Jae Feb 05 '12 at 16:14
  • 1
    Oh, true. Well, after you fix that (which I see that you've already done), this would have been your next problem. – BJ Homer Feb 05 '12 at 16:47
0

My guess is that you should either wait until the stream is finished to do CFRelease(writeStreamRef), or do a __bridge_transfer to transfer the ownership over to ftpStream before you release your writeStreamRef

Audun Kjelstrup
  • 1,430
  • 8
  • 13
  • a wrong guess. the reason was the wrong declaration of the FTPUploads as a local variable only (see UploadDatasetVC creation code in original question). still you are right with respect to my wrong '__bridge_transfer' which would have turnged out to be a bug. i just fixed that, thanks! – m Jae Feb 05 '12 at 16:13