Commit ca7ab7c6 authored by Mark Cogan's avatar Mark Cogan Committed by Commit Bot

[iOS] add CallableDispatcher() helper.

Currently the usual way to turn a CommandDispatcher pointer into
something that can have methods called on it is to static_cast() it
into id<Protocols>, which compiles but has no runtime correct check.

+conformsToProtocol: can't be used here, since that just checks a class
interface, and doesn't work on a dynamic object like CommandDispatcher.

This CL adds a function-like helper macro for use with CommandDispatcher.
It does both typecasting and a runtime check that the dispatcher can
handle the methods in the protocol.

A macro is necessary to generate the correct static_cast expression that
matches the protocol whose methods are being checked. Believe me, if
there was another way to do this, I'd use it,

The helper matches the behavior of ObjCCastStrict, returning nil and
DCHECKing if the dispatcher doesn't handle the specified protocol.

Note that while the dispatcher's startDispatchingToTarget:forProtocol:
method doesn't handle protocols that the passed protocol conforms to,
CallableDispatcher does, so composite protocols can be used when the
callable needs to conform to multiple protocols.

Change-Id: I49953ed89d76cbe6e714abec226161f927ebb60f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1866751
Commit-Queue: Mark Cogan <marq@chromium.org>
Reviewed-by: default avatarStepan Khapugin <stkhapugin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707888}
parent e23dd072
......@@ -9,10 +9,32 @@
// CommandDispatcher allows coordinators to register as command handlers for
// specific selectors. Other objects can call these methods on the dispatcher,
// which in turn will forward the call to the registered handler. In addition,
// coordinators can register MetricsRecorders with selectors so that when a
// a selector is invoked on the dispatcher, the MetricsRecorder is also
// notified.
// which in turn will forward the call to the registered handler.
// While CommandDispatcher conforms functionally to any protocols it is
// dispatching for, the compiler doesn't (and can't) know that. To call
// dispatched methods on a dispatcher, it's best to use a typecast to an
// id pointer conforming to the relevant protocols. Such a pointer should also
// be passed into objects that need to call, but not configure, the dispatcher
// (anything other than a coordinator). To create such a pointer in a way that
// both compiles and is checked for correctness at runtime, use the provided
// function-like macro CallableDispatcher, defined below. Usage is as follows:
//
// id<SomeProtocol> callable = CallableDispatcher(dispatcher, SomeProtocol);
//
// |dispatcher| should be a CommandDispatcher object, and SomeProtocol is
// the *name* of a protocol (not a string, not a Protocol* pointer, and not
// an @protocol() expression to generate one).
//
// This will typecast |dispatcher| to an id<SomeProtocol> (for compile-time
// type checking), and verify that |dispatcher| is currently dispatching
// for |protocol| (for run-time verification). If |dispatcher| isn't dispatching
// for |protocol|, CallableDispatcher() returns nil and DCHECKs.
//
#define CallableDispatcher(Dispatcher, ProtocolName) \
static_cast<id<ProtocolName>>( \
[Dispatcher strictCallableForProtocol:@protocol(ProtocolName)])
@interface CommandDispatcher : NSObject
// Registers the given |target| to receive forwarded messages for the given
......@@ -35,6 +57,14 @@
// Removes all forwarding registrations for the given |target|.
- (void)stopDispatchingToTarget:(id)target;
// YES if the dispatcher is currently dispatching for |protocol|, including
// (recursively) any protocols that |protocol| conforms to.
- (BOOL)dispatchingForProtocol:(Protocol*)protocol;
// Returns the reciever if it is dispatching for |protocol|, and CHECK()s
// otherwise.
- (CommandDispatcher*)strictCallableForProtocol:(Protocol*)protocol;
@end
#endif // IOS_CHROME_BROWSER_UI_COMMANDS_COMMAND_DISPATCHER_H_
......@@ -71,6 +71,49 @@
}
}
- (BOOL)dispatchingForProtocol:(Protocol*)protocol {
// Special-case the NSObject protocol.
if ([@"NSObject" isEqualToString:NSStringFromProtocol(protocol)]) {
return YES;
}
unsigned int methodCount;
objc_method_description* requiredInstanceMethods =
protocol_copyMethodDescriptionList(protocol, YES /* isRequiredMethod */,
YES /* isInstanceMethod */,
&methodCount);
BOOL conforming = YES;
for (unsigned int i = 0; i < methodCount; i++) {
SEL selector = requiredInstanceMethods[i].name;
if (_forwardingTargets.find(selector) == _forwardingTargets.end()) {
conforming = NO;
break;
}
}
free(requiredInstanceMethods);
if (!conforming)
return NO;
unsigned int protocolCount;
Protocol* __unsafe_unretained _Nonnull* _Nullable conformedProtocols =
protocol_copyProtocolList(protocol, &protocolCount);
for (unsigned int i = 0; i < protocolCount; i++) {
if (![self dispatchingForProtocol:conformedProtocols[i]]) {
conforming = NO;
break;
}
}
free(conformedProtocols);
return conforming;
}
- (CommandDispatcher*)strictCallableForProtocol:(Protocol*)protocol {
CHECK([self dispatchingForProtocol:protocol])
<< "Dispatcher failed protocol confromance";
return self;
}
#pragma mark - NSObject
// Overridden to forward messages to registered handlers.
......
......@@ -22,6 +22,18 @@
- (void)showMore;
@end
@protocol HideProtocol
- (void)hide;
- (void)hideMore;
@end
@protocol CompositeProtocolWithMethods <HideProtocol>
- (void)doCompositeThings;
@end
@protocol EmptyContainerProtocol <CompositeProtocolWithMethods, ShowProtocol>
@end
// A handler with methods that take no arguments.
@interface CommandDispatcherTestSimpleTarget : NSObject<ShowProtocol>
......@@ -366,3 +378,61 @@ TEST_F(CommandDispatcherTest, RespondsToSelector) {
EXPECT_TRUE(
[dispatcher respondsToSelector:@selector(stopDispatchingForSelector:)]);
}
TEST_F(CommandDispatcherTest, DispatchingForProtocol) {
id dispatcher = [[CommandDispatcher alloc] init];
NSObject* target = [[NSObject alloc] init];
// Check that -dispatchingForProtocol tracks simple stop/start.
EXPECT_FALSE([dispatcher dispatchingForProtocol:@protocol(HideProtocol)]);
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(HideProtocol)];
EXPECT_TRUE([dispatcher dispatchingForProtocol:@protocol(HideProtocol)]);
[dispatcher stopDispatchingForProtocol:@protocol(HideProtocol)];
EXPECT_FALSE([dispatcher dispatchingForProtocol:@protocol(HideProtocol)]);
// Check that -dispatchingForProtocol handles a conformed protocol.
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(CompositeProtocolWithMethods)];
EXPECT_FALSE([dispatcher
dispatchingForProtocol:@protocol(CompositeProtocolWithMethods)]);
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(HideProtocol)];
EXPECT_TRUE([dispatcher
dispatchingForProtocol:@protocol(CompositeProtocolWithMethods)]);
// Check that -dispatchingForProtocol doesn't have a problem with a protocol
// that also conforms to NSObject.
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(ShowProtocol)];
EXPECT_TRUE([dispatcher dispatchingForProtocol:@protocol(ShowProtocol)]);
// Check that conforming to all of the conformed protocols in a protocol with
// no methods is the same as conforming to that protocol.
EXPECT_TRUE(
[dispatcher dispatchingForProtocol:@protocol(EmptyContainerProtocol)]);
// Check that stopping dispatch to a protocol doesn't stop dispatch to its
// conformed protocols.
[dispatcher
stopDispatchingForProtocol:@protocol(CompositeProtocolWithMethods)];
EXPECT_TRUE([dispatcher dispatchingForProtocol:@protocol(HideProtocol)]);
}
TEST_F(CommandDispatcherTest, CallableDispatcher) {
CommandDispatcher* dispatcher = [[CommandDispatcher alloc] init];
NSObject* target = [[NSObject alloc] init];
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(ShowProtocol)];
id<ShowProtocol> callable = CallableDispatcher(dispatcher, ShowProtocol);
EXPECT_EQ(callable, dispatcher);
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(HideProtocol)];
[dispatcher startDispatchingToTarget:target
forProtocol:@protocol(CompositeProtocolWithMethods)];
id<EmptyContainerProtocol> container_callable =
CallableDispatcher(dispatcher, EmptyContainerProtocol);
EXPECT_EQ(container_callable, dispatcher);
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment