Implementing callback with NSInvocation
Let’s say we wanted to write a callback with NSInvocation
. Why would you need that? Maybe you do not like the block syntax, or maybe you do not like how blocks capturing works, or maybe you’re working with a really old codebase which can not use blocks.
Anatomy of a callback
Anyone who has been writing software for a while knows what a callback is. I struggled a bit when I first encountered the concept for the first time. In my novice head programs were simple chain of procedures that execute one after the other. Like, flowA -> flowB. I had never experienced a flow where a procedure would need to be interrupted for a while and then resume where it left. Like flow_begin -> interrupt -> flow_end.
I distinctly remember the day I first encountered the signal
API.
void (*signal (int, void (*)(int))) (int);
If this is the first time you’re looking at the monstrosity, let me break it down in simpler words. signal
is a function that takes in 2 arguments, a int
signal value and a function pointer of type void (*)(int)
and returns a function pointer of type void (*)(int)
. The function pointers here represent callbacks.
Another simpler example is of the C standard library quicksort:
void qsort( void *ptr, size_t count, size_t size,
int (*comp)(const void *, const void *) );
The interesting thing here is that qsort
takes in a comp
as a callback. So the algorithm can simply work on some abstract level as long as a user provides the comparison function.
Callback with Delegation
All modern languages provide a simpler way to represent callbacks. Objective-C since 2011 supports blocks, which although looks very similar to C function pointer but is a lot more than just simple callbacks. A block also capture the current state of stack implicitly that helps a lot with sharing data between flow_begin, interrupt and flow_end.
Before blocks, there were some old school ways of handling callbacks in Objc. One the most used pattern was to use delegation, which went something like:
- Define a
protocol
for the interrupt - Flow is injected with a instance that conforms to
protocol
, called a delegate - Whenever required the flow calls the delegate methods.
As an example, the qsort
above might be written in Objc as:
@protocol PLComparison <NSObject>
- (int)compare:(void *)first to:(void *)second;
@end
@interface PLQSort : NSObject
- (void)qsortData:(void *)data count:(size_t)count
compareDelegate:(id<PLComparison>)delegate;
@end
Callback with Block
These days blocks are the most accepted way of implementing a callback. This is even more true with swift. The idea is very simple.
- Define a method signature
typedef void(^PLCalcAddCompletion)(NSInteger);
- Define a method that implements the flow
@interface PLNonEscapingCalc : NSObject
- (void)add:(NSInteger)first
with:(NSInteger)second
block:(PLCalcAddCompletion)block;
@end
@implementation PLNonEscapingCalc
- (void)add:(NSInteger)first
with:(NSInteger)second
block:(PLCalcAddCompletion)block;
{
NSInteger sum = first + second;
block(sum);
}
@end
- Call the flow and inject the callback
- (void)run
{
PLNonEscapingCalc *calc = [[PLNonEscapingCalc alloc] init];
[calc add:10 with:20 block:^(NSInteger sum) {
[self print:sum];
}];
[calc release];
}
- (void)print:(NSInteger)sum
{
NSLog(@"sum: %ld\n", sum);
}
With blocks the PLNonEscapingCalc
has to know nothing about its caller, as all the type information it needs is available right there in the signature.
Callback with NSInvocation
With NSInvocation
the flow is very much the same.
- Define a method that implements the flow. But since this is Objc, let the runtime figure out the callback signature for later.
@interface PLNonEscapingCalc : NSObject
- (void)add:(NSInteger)first
with:(NSInteger)second
invocation:(NSInvocation *)invocation;
@end
@implementation PLNonEscapingCalc
- (void)add:(NSInteger)first
with:(NSInteger)second
invocation:(NSInvocation *)invocation;
{
NSInteger sum = first + second;
[invocation setArgument:&sum atIndex:2];
[invocation invoke];
}
@end
Notice that we set the argument as index 2. This because in Objc index 0 and 1 are reserved for receiver and selector. We have to provide those values from the other end:
- (void)run
{
PLNonEscapingCalc *calc = [[PLNonEscapingCalc alloc] init];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:
@selector(print:)]];
[invocation setTarget:self];
[invocation setSelector:@selector(print:)];
[calc add:30 with:40 invocation:invocation];
[calc release];
}
With NSInvocation
approach the PLNonEscapingCalc
knows absolutely nothing about its caller. The only thing it cares about is to set data at index 2. So, we rely on a proper documentation on make sure the contract is valid.
Build a networking layer
For a final exercise, lets try to build a NSURLSession
like API but based on NSInvocation
instead of blocks.
First we need something to hold the NSInvocation
and the NSData
as the NSURLSessionDataDelegate
might return partial data. From the doc:
This delegate method may be called more than once, and each call provides only data received since the previous call. The app is responsible for accumulating this data if needed.
@interface PLNetworkTask : NSObject
{
NSURLSessionTask *_task;
NSMutableData *_data;
NSInvocation *_invocation;
}
@property (nonatomic, readonly) NSURLSessionTask *task;
@end
@implementation PLNetworkTask
+ (instancetype)futureWithTask:(NSURLSessionTask *)task
invocation:(NSInvocation *)invocation
{
return [[[self alloc] initWithTask:task
invocation: invocation] autorelease];
}
- (instancetype)initWithTask:(NSURLSessionTask *)task
invocation:(NSInvocation *)invocation
{
self = [super init];
if (self) {
_task = [task retain];
_data = [[NSMutableData alloc] init];
_invocation = [invocation retain];
}
return self;
}
- (void)dealloc
{
[_invocation release];
[_task release];
[_data release];
[super dealloc];
}
- (void)appendData:(NSData *)data
{
[_data appendData:data];
}
- (void)completeWithError:(NSError *)error
{
[_invocation setArgument:&_data atIndex:2];
[_invocation setArgument:&error atIndex:3];
[_invocation invoke];
}
@end
Next we need something like a NSURLSession
than manages many PLNetworkTask
.
@interface PLNetworkSession() <NSURLSessionDataDelegate>
{
NSURLSession *_session;
NSMutableArray *_tasks;
NSOperationQueue *_sessionQueue;
}
@end
@implementation PLNetworkSession
- (instancetype)init
{
self = [super init];
if (self) {
_sessionQueue = [[NSOperationQueue alloc] init];
[_sessionQueue setMaxConcurrentOperationCount:1];
_tasks = [[NSMutableArray alloc] init];
_session = [[NSURLSession sessionWithConfiguration:
[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self
delegateQueue:_sessionQueue]
retain];
}
return self;
}
- (void)dealloc
{
[_sessionQueue release];
[_tasks release];
[_session release];
[super dealloc];
}
- (void)dispatchRequest:(NSURLRequest *)request
invocation:(NSInvocation *)invocation
{
NSAssert([invocation target] != nil, @"Target should be set");
NSAssert([invocation selector] != nil, @"Selector should be set");
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
[_tasks addObject:[PLNetworkTask futureWithTask:task
invocation: invocation]];
[task resume];
}
- (PLNetworkTask *)futureWithTask:(NSURLSessionTask *)task
{
for (PLNetworkTask *future in _tasks) {
if ([future task] == task) {
return future;
}
}
return nil;
}
#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;
{
[[self futureWithTask:dataTask] appendData:data];
}
#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error;
{
PLNetworkTask *future = [self futureWithTask:task];
[future completeWithError:error];
[_tasks removeObject:future];
}
@end
And finally we can start using PLNetworkSession
to schedule async tasks
- (void)handleData:(NSData *)data error:(NSError *)error
{
if (!data) {
return;
}
NSString *dataStr = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(@"data %@", dataStr);
[dataStr release];
}
- (void)runNetwork
{
self.session = [[PLNetworkSession alloc] init];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector
:@selector(handleData:error:)]];
[invocation setTarget:self];
[invocation setSelector:@selector(handleData:error:)];
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:
@"https://jsonplaceholder.typicode.com/todos/1"]];
[_session dispatchRequest:request invocation:invocation];
}
Output
data {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
Afterwords
This was more or less just an demo on how NSInvocation
can be used to build a callback pattern. Swift does not even exposes NSInvocation
so one has to use some other mechanism when working with Swift. I personally have no favorites, like every other thing when writing softwares we have to be judicious when selecting the right tool for the job. If for example there is no data transfer, rather only control transfer among several procedures, NSInvocation
could be a better fitting tool. Like say the libdispatch library.