diff --git a/Utilities/GCDMulticastDelegate.h b/Utilities/GCDMulticastDelegate.h index 3c48a7ee72..18ae32677d 100644 --- a/Utilities/GCDMulticastDelegate.h +++ b/Utilities/GCDMulticastDelegate.h @@ -1,6 +1,6 @@ #import -@class GCDMulticastDelegateEnumerator; +@class GCDMulticastDelegateEnumerator, GCDMulticastDelegateInvocationContext; /** * This class provides multicast delegate functionality. That is: @@ -54,5 +54,44 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr ofClass:(Class)aClass; - (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr forSelector:(SEL)aSelector; +@end + +/** + * A helper class for propagating custom context across multicast delegate invocations. + * + * This class serves 2 main purposes: + * - provides an auxiliary path of custom data delivery to the invoked delegate methods + * - makes it possible to track the delegate method invocation completion + * + * The context propagates along the cascade of invocations, i.e. when a delegate method calls another multicast delegate, + * that subsequent invocation belongs to the same context. This is particularly relevant w.r.t. the xmpp framework + * architecture where a scenario with two layers of delegation is common: stream -> module and module -> application. + * The propagating context is what enables the framework to deliver stream event-related information across modules + * to the application callbacks. + * + * The default context propagation junctions are invocation forwarding and delegate enumerator creation. As long as + * they are executed under an existing context, the propagation is automatic. + * + * A manual propagation scenario (e.g. asynchronous message processing within a module) would consist of the following steps: + * 1. Capturing the context object while still in the delegate callback context with @c currentContext. + * 2. Entering the captured context's @c continuityGroup. + * 3. Restoring the context on an arbitrary queue with @c becomeCurrentOnQueue:forActionWithBlock: + * 4. Leaving the @c continuityGroup within the action block submitted to @c becomeCurrentOnQueue:forActionWithBlock: + * + * Steps 2. and 4. are only required if @c becomeCurrentOnQueue:forActionWithBlock: itself is invoked asynchronously + * (e.g. in a network or disk IO completion callback). + */ +@interface GCDMulticastDelegateInvocationContext : NSObject + +@property (nonatomic, strong, readonly) id value; +@property (nonatomic, strong, readonly) dispatch_group_t continuityGroup; + ++ (instancetype)currentContext; + +- (instancetype)initWithValue:(id)value; +- (instancetype)init NS_UNAVAILABLE; + +- (void)becomeCurrentOnQueue:(dispatch_queue_t)queue forActionWithBlock:(dispatch_block_t)block; + @end NS_ASSUME_NONNULL_END diff --git a/Utilities/GCDMulticastDelegate.m b/Utilities/GCDMulticastDelegate.m index 1785c274d4..4d9f5f2ae0 100644 --- a/Utilities/GCDMulticastDelegate.m +++ b/Utilities/GCDMulticastDelegate.m @@ -9,6 +9,10 @@ #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif +static void * const GCDMulticastDelegateInvocationContextKey = (void *)&GCDMulticastDelegateInvocationContextKey; + +static void GCDMulticastDelegateInvocationContextLeave(void *contextPtr); + /** * How does this class work? * @@ -76,6 +80,12 @@ - (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes; @end +@interface GCDMulticastDelegateInvocationContext () + +- (dispatch_queue_t)transferContextToTargetQueue:(dispatch_queue_t)targetQueue; + +@end + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -227,7 +237,22 @@ - (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector - (GCDMulticastDelegateEnumerator *)delegateEnumerator { - return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes]; + NSMutableArray *actualDelegateNodes; + + GCDMulticastDelegateInvocationContext *currentInvocationContext = [GCDMulticastDelegateInvocationContext currentContext]; + if (currentInvocationContext) { + actualDelegateNodes = [[NSMutableArray alloc] init]; + for (GCDMulticastDelegateNode *node in delegateNodes) { + dispatch_queue_t contextTransferQueue = [currentInvocationContext transferContextToTargetQueue:node.delegateQueue]; + GCDMulticastDelegateNode *contextTransferNode = [[GCDMulticastDelegateNode alloc] initWithDelegate:node.delegate + delegateQueue:contextTransferQueue]; + [actualDelegateNodes addObject:contextTransferNode]; + } + } else { + actualDelegateNodes = delegateNodes; + } + + return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:actualDelegateNodes]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector @@ -276,7 +301,10 @@ - (void)forwardInvocation:(NSInvocation *)origInvocation NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; - dispatch_async(node.delegateQueue, ^{ @autoreleasepool { + GCDMulticastDelegateInvocationContext *currentContext = [GCDMulticastDelegateInvocationContext currentContext]; + dispatch_queue_t invocationQueue = [currentContext transferContextToTargetQueue:node.delegateQueue] ?: node.delegateQueue; + + dispatch_async(invocationQueue, ^{ @autoreleasepool { [dupInvocation invokeWithTarget:nodeDelegate]; @@ -652,3 +680,47 @@ - (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr for } @end + +@implementation GCDMulticastDelegateInvocationContext + ++ (instancetype)currentContext +{ + void *contextPtr = dispatch_get_specific(GCDMulticastDelegateInvocationContextKey); + return contextPtr ? CFBridgingRelease(CFRetain(contextPtr)) : nil; +} + +- (instancetype)initWithValue:(id)value +{ + self = [super init]; + if (self) { + _continuityGroup = dispatch_group_create(); + _value = value; + } + return self; +} + +- (void)becomeCurrentOnQueue:(dispatch_queue_t)queue forActionWithBlock:(dispatch_block_t)block +{ + dispatch_async([self transferContextToTargetQueue:queue], block); +} + +- (dispatch_queue_t)transferContextToTargetQueue:(dispatch_queue_t)targetQueue +{ + dispatch_group_enter(self.continuityGroup); + + dispatch_queue_t contextTransferQueue = dispatch_queue_create_with_target("GCDMulticastDelegateInvocationContext.contextTransferQueue", nil, targetQueue); + dispatch_queue_set_specific(contextTransferQueue, + GCDMulticastDelegateInvocationContextKey, + (void *)CFBridgingRetain(self), + GCDMulticastDelegateInvocationContextLeave); + + return contextTransferQueue; +} + +@end + +static void GCDMulticastDelegateInvocationContextLeave(void *contextPtr) +{ + GCDMulticastDelegateInvocationContext *context = CFBridgingRelease(contextPtr); + dispatch_group_leave(context.continuityGroup); +}