Subclassing NSInputStream

Cocoa’s NSInputStream is great, but sometimes it doesn’t have all the functionality you need. For example, you might want to dynamically encrypt the file as you were streaming it off the disk, or you might want to put up a progress bar indicating how far the input stream had progressed through a large file. NSInputStream doesn’t support either of these options natively, but this sounds like a great place for an NSInputStream subclass, right?

Wrong.

Or rather, right, with the caveat that subclasses of NSInputStream don’t work correctly when used with Cocoa’s URL loading mechanism. (Apple folks: See rdar://problem/3222783.)

(UPDATE: This appears to have been fixed in the years since I wrote it. I’m leaving this here for historical curiosity, but you should be able to just subclass an NSInputStream as documented now.)

The Problem

When you make an NSInputStream subclass and try to pass it to -[NSURLRequest setHTTPBodyStream:], your app will quickly crash with an ‘unrecognized selector’ for the following method:

- (void) _scheduleInCFRunLoop:forMode:(CFStringRef)inMode

If you implement that, you’ll then get another unrecognized selector:

- (BOOL) _setCFClientFlags:(CFOptionFlags)inFlags
callback:(CFReadStreamClientCallBack)inCallback
context:(CFStreamClientContext *)inContext

If you give that one an empty implementation too (return YES;) and the file you’re uploading is small, you may manage to make it to a third unimplemented selector:

- (void) _unscheduleFromCFRunLoop:(CFRunLoopRef)inRunLoop forMode:(CFStringRef)inMode

Unfortunately, while you can avoid the unrecognized selector crashes by implementing these methods, you’re very unlikely to get through your whole stream. Further, Apple engineers have stated that such naive implementations are “definitely not safe” and will likely lead to failing in “strange and unexpected ways.”

The backstory

The real question here is what these methods are for in the first place. It turns out that these three methods are simply the toll-free bridging versions of CFReadStreamScheduleWithRunLoop, CFReadStreamSetClient, and CFReadStreamUnscheduleFromRunLoop, respectively. Calling CFReadStreamScheduleWithRunLoop from _scheduleInCFRunLoop:..., for example, is a quick way to infinite recursion.

The NSStream documentation indicates that subclasses must override -(void)scheduleInRunLoop:forMode: and -(void)removeFromRunLoop:forMode:. Why? Because the stream’s delegate usually needs to be notified when there are bytes available to be read, and that getting that information often requires being scheduled on a run loop. Our three mystery methods serve the same purpose.

NSInputStream is toll-free bridged with CFReadStream. Mostly. From the CFReadStream Reference:

CFReadStream is “toll-free bridged” with its Cocoa Foundation counterpart, NSInputStream. This means that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object. Therefore, in a method where you see an NSInputStream * parameter, you can pass in a CFReadStreamRef, and in a function where you see a CFReadStreamRef parameter, you can pass in an NSInputStream instance. Note, however, that you may have either a delegate or callbacks but not both.

These methods are required to support the CFReadStream client callbacks, which are distinct from the delegate callbacks.

The solution

-[NSInputStream _scheduleInCFRunLoop:forMode:] is the equivalent of CFReadStreamScheduleWithRunLoop for your stream. Do whatever you need to do so that you can give proper kCFStreamEventHasBytesAvailable notifications (and any other notifications requested) at the proper time. That may involve scheduling a timer on the run loop, or if your subclass is just wrapping a vanilla NSInputStream, simply scheduling that stream on the run loop. Implement this method as if you were implementing CFReadStreamScheduleWithRunLoop for your stream

-[NSInputStream _setCFClientFlags:callback:context:] is the equivalent of CFReadStreamSetClient, you need to do a few things. If the context and callback arguments are not NULL, the caller is trying to set up a callback client.

  1. Inspect the flags, and record which notifications are requested. The possible values are listed in the CFStream Event Type Constants documentation.
  2. Copy the pointer to the callback function. You’ll need to use it later.
  3. Copy the context. The documentation for CFReadStreamSetClient indicates that the context struct passed by the caller should be copied, and that the caller is not responsible for preserving it. memcpy(&myLocalContextCopy, thePassedContext, sizeof( CFStreamClientContext)) works just fine.
  4. Retain the context->info. The context struct includes a void *info member and a CFAllocatorRetainCallBack retain member. Call the retain function on the info pointer (if the retain function is not nil).

If the context and callback parameters are NULL, then the caller is removing the callback client, and you need to do the following:

  1. Call the release function (from the context you previously copied) on the info pointer in that context.
  2. Remove your copy of the context and callback; they are no longer needed.

Finally, return YES to indicate that the asynchronous scheduling was successful.

-[NSInputStream _unscheduleFromCFRunLoop:forMode] is the equivalent of CFReadStreamUnscheduleFromRunLoop. You should remove anything you scheduled in the _scheduleInCFRunLoop:forMode: method.

Once you’ve done these, make sure you’ve implemented the other methods required for subclasses as documented in the NSInputStream and NSStream reference. Now, when you pass off your NSInputStream subclass to NSURLRequest, your stream will be scheduled on the run loop and the client callbacks will be set up. Once you’re scheduled on the run loop, you should be able to notify your client when you have bytes available to read, when you’ve reached the end of the file, and when an error has occurred. Make sure you’re passing those along correctly, and everything will be good to go.

Sample

I’ve put together a sample implementation of an NSInputStream subclass that demonstrates what I’ve presented. HSCountingInputStream is a simple class that wraps an NSInputStream, counting the number of instances of a given character have passed through it. Nearly every call is simply passed through to the underlying NSInputStream; the only real code of interest is in the -read:maxLength: and in the undocumented methods discussed above.

I’ll keep an eye on the comments, so if you have any questions, please let me know. I’ll probably write up another blog here shortly about how I figured all of this out, for those who are interested.

6 responses to “Subclassing NSInputStream”

  1. Hi BJ, Thanks for the article. This is very useful.I'm new to iOS programming. I am trying to implement an NSInputStream subclass that I can use to mock or simulate data streaming in from a socket connection, rather than having to set up a server.How would I do this? Do you know of any sample code that shows how to do something like this?For example, what should my scheduleInRunLoop:forMode: method look like? I'm looking for some sample code of a completely custom NSInputStream sublcass (one that doesn't just call the super's routines).

    Like

  2. Ryan,If you already know the full content of what you want to stream from the mock socket connection, you could just write it to a file and then use a vanilla NSInputStream to read it from the file.If you do need a subclass, you may be able to get away with an empty scheduleInRunLoop implementation as long as you have some other way of making sure your client or delegate is notified when new bytes are available. One thing you could do is just notify the client that more bytes are available immediately after every read. If you do that, you don't need to do anything in the schedule and unschedule methods at all.Keep an eye on my HSCountingInputStream sample code; I'll add a random data generator input stream sample there soon.

    Like

  3. Hey, nice work 🙂 I'm working through this problem right now. One thing I noticed while looking at your sample code:- (void)setDelegate:(id)aDelegate { if (aDelegate == nil) { delegate = self; } delegate = aDelegate;}needs an else, right?

    Like

  4. toadgee:You're right. I think I originally intended to do `aDelegate = self` in the if block, and then just assign `delegate = aDelegate` unconditionally. But I'm not sure if that's any less confusing.Regardless, I've fixed it on github by adding an `else`. Thanks for catching that.

    Like

  5. Hm, interesting. I'm seeing a couple of strange behaviors you may not notice unless you run under Instruments & leaks.I'm also noticing on 10.6(.8) that my stream never seems to get closed or cleaned up — but on 10.7(.1) this seems to be fixed. Just a warning for those who might be allocating a large buffer of memory as an internal buffer. The additional retain comes from down within the depths of the CFNetworking layer (IIRC) in spoolingCreate. In my case, once I was sure that I had finished with my internal buffer, I got rid of it, so the leak wouldn't be as bad on 10.6.Additionally, on both 10.6 & 10.7, I get called _scheduleInCFRunLoop multiple times with the same runLoop & mode, so I needed to make sure that any old _runLoopSource I had previously created I also CFReleased. I took the stance of unscheduling myself from the old CFRunLoop w/ the old mode as well.

    Like

  6. How does this work with Apple's approval process? Is there a chance of getting a rejection for this code?My concern is about undocumented CF _ APIs.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: