Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selector Signal Interoperability. #33

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 41 additions & 39 deletions ReactiveObjC/NSObject+RACSelectorSignal.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,9 @@

@implementation NSObject (RACSelectorSignal)

static BOOL RACForwardInvocation(id self, NSInvocation *invocation) {
SEL aliasSelector = RACAliasForSelector(invocation.selector);
RACSubject *subject = objc_getAssociatedObject(self, aliasSelector);

Class class = object_getClass(invocation.target);
BOOL respondsToAlias = [class instancesRespondToSelector:aliasSelector];
if (respondsToAlias) {
invocation.selector = aliasSelector;
[invocation invoke];
}

if (subject == nil) return respondsToAlias;

[subject sendNext:invocation.rac_argumentsTuple];
return YES;
}

static void RACSwizzleForwardInvocation(Class class) {
Class superclass = class_getSuperclass(class);
SEL forwardInvocationSEL = @selector(forwardInvocation:);
Method forwardInvocationMethod = class_getInstanceMethod(class, forwardInvocationSEL);

// Preserve any existing implementation of -forwardInvocation:.
void (*originalForwardInvocation)(id, SEL, NSInvocation *) = NULL;
if (forwardInvocationMethod != NULL) {
originalForwardInvocation = (__typeof__(originalForwardInvocation))method_getImplementation(forwardInvocationMethod);
}

// Set up a new version of -forwardInvocation:.
//
Expand All @@ -75,26 +52,47 @@ static void RACSwizzleForwardInvocation(Class class) {
// was no existing implementation, throw an unrecognized selector
// exception.
id newForwardInvocation = ^(id self, NSInvocation *invocation) {
BOOL matched = RACForwardInvocation(self, invocation);
if (matched) return;
SEL originalSelector = invocation.selector;
SEL aliasSelector = RACAliasForSelector(invocation.selector);
RACSubject* subject = objc_getAssociatedObject(self, aliasSelector);

BOOL responseToSelector = [superclass instancesRespondToSelector:originalSelector];
BOOL forward = NO;
if (responseToSelector) {
Method method = class_getInstanceMethod(superclass, invocation.selector);
IMP impl = method_getImplementation(method);

if (impl != _objc_msgForward) {
class_replaceMethod(object_getClass(self), aliasSelector, impl, method_getTypeEncoding(method));
Copy link
Member Author

@andersio andersio Nov 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks nasty, but there is no other reliable way to invoke a specific IMP with NSInvocation in the standard toolbox.

invocation.selector = aliasSelector;
[invocation invoke];
} else {
forward = YES;
}
}

if (originalForwardInvocation == NULL) {
[self doesNotRecognizeSelector:invocation.selector];
} else {
originalForwardInvocation(self, forwardInvocationSEL, invocation);
if (forward || (!responseToSelector && subject == nil)) {
struct objc_super target = {
.super_class = superclass,
.receiver = self,
};

void*(*superForwardInvocation)(struct objc_super *, SEL, NSInvocation*) = (__typeof__(superForwardInvocation)) objc_msgSendSuper;
superForwardInvocation(&target, forwardInvocationSEL, invocation);
}

if (subject != nil) {
[subject sendNext:invocation.rac_argumentsTuple];
}
};

class_replaceMethod(class, forwardInvocationSEL, imp_implementationWithBlock(newForwardInvocation), "v@:@");
}

static void RACSwizzleRespondsToSelector(Class class) {
Class superclass = class_getSuperclass(class);
SEL respondsToSelectorSEL = @selector(respondsToSelector:);

// Preserve existing implementation of -respondsToSelector:.
Method respondsToSelectorMethod = class_getInstanceMethod(class, respondsToSelectorSEL);
BOOL (*originalRespondsToSelector)(id, SEL, SEL) = (__typeof__(originalRespondsToSelector))method_getImplementation(respondsToSelectorMethod);

// Set up a new version of -respondsToSelector: that returns YES for methods
// added by -rac_signalForSelector:.
//
Expand All @@ -110,10 +108,17 @@ static void RACSwizzleRespondsToSelector(Class class) {
if (objc_getAssociatedObject(self, aliasSelector) != nil) return YES;
}

return originalRespondsToSelector(self, respondsToSelectorSEL, selector);
struct objc_super target = {
.super_class = superclass,
.receiver = self,
};

BOOL(*superRespondsToSelector)(struct objc_super *, SEL, SEL) = (__typeof__(superRespondsToSelector)) objc_msgSendSuper;

return superRespondsToSelector(&target, respondsToSelectorSEL, selector);
};

class_replaceMethod(class, respondsToSelectorSEL, imp_implementationWithBlock(newRespondsToSelector), method_getTypeEncoding(respondsToSelectorMethod));
class_replaceMethod(class, respondsToSelectorSEL, imp_implementationWithBlock(newRespondsToSelector), "v@::");
}

static void RACSwizzleGetClass(Class class, Class statedClass) {
Expand Down Expand Up @@ -224,9 +229,6 @@ static void RACCheckTypeEncoding(const char *typeEncoding) {

RACCheckTypeEncoding(typeEncoding);

BOOL addedAlias __attribute__((unused)) = class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), class);

// Redefine the selector to call -forwardInvocation:.
class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));
}
Expand Down
149 changes: 146 additions & 3 deletions ReactiveObjCTests/NSObjectRACSelectorSignalSpec.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
@import Quick;
@import Nimble;

#import <objc/message.h>

#import "RACTestObject.h"
#import "RACSubclassObject.h"

#import <ReactiveObjC/EXTScope.h>

#import "NSObject+RACDeallocating.h"
#import "NSObject+RACPropertySubscribing.h"
#import "NSObject+RACSelectorSignal.h"
Expand Down Expand Up @@ -137,7 +141,11 @@ - (id)objectValue;
qck_it(@"should send arguments for invocation and invoke the original method on previously KVO'd receiver", ^{
RACTestObject *object = [[RACTestObject alloc] init];

[[RACObserve(object, objectValue) publish] connect];
__block id latestValue;
[[[RACObserve(object, objectValue) publish] autoconnect] subscribeNext:^(id objectValue) {
latestValue = objectValue;
}];
expect(latestValue).to(beNil());

__block id key;
__block id value;
Expand All @@ -152,6 +160,8 @@ - (id)objectValue;
expect(object.objectValue).to(equal(@YES));
expect(object.secondObjectValue).to(equal(@"Winner"));

expect(latestValue).to(equal(@YES));

expect(value).to(equal(@YES));
expect(key).to(equal(@"Winner"));
});
Expand All @@ -166,14 +176,20 @@ - (id)objectValue;
key = x.second;
}];

[[RACObserve(object, objectValue) publish] connect];
__block id latestValue;
[[[RACObserve(object, objectValue) publish] autoconnect] subscribeNext:^(id objectValue) {
latestValue = objectValue;
}];
expect(latestValue).to(beNil());

[object setObjectValue:@YES andSecondObjectValue:@"Winner"];

expect(@(object.hasInvokedSetObjectValueAndSecondObjectValue)).to(beTruthy());
expect(object.objectValue).to(equal(@YES));
expect(object.secondObjectValue).to(equal(@"Winner"));

expect(latestValue).to(equal(@YES));

expect(value).to(equal(@YES));
expect(key).to(equal(@"Winner"));
});
Expand All @@ -193,7 +209,7 @@ - (id)objectValue;

expect(@([object respondsToSelector:selector])).to(beTruthy());
});

qck_it(@"should properly implement -respondsToSelector: when called on signalForSelector'd receiver that has subsequently been KVO'd", ^{
RACTestObject *object = [[RACTestObject alloc] init];

Expand Down Expand Up @@ -306,6 +322,133 @@ - (id)objectValue;
});
});

qck_describe(@"interoperability", ^{
__block BOOL invoked;
__block RACTestObject * object;
__block Class originalClass;

qck_beforeEach(^{
invoked = NO;
object = [[RACTestObject alloc] init];
originalClass = RACTestObject.class;
});

qck_it(@"should invoke the swizzled `forwardInvocation:` on an instance isa-swizzled by both RAC and KVO.", ^{
[[RACObserve(object, objectValue) publish] connect];
[object rac_signalForSelector:@selector(lifeIsGood:)];

SEL swizzledSelector = @selector(lifeIsGood:);

// Redirect `swizzledSelector` to the forwarding machinery.
Method method = class_getInstanceMethod(originalClass, swizzledSelector);
const char *typeDescription = (char *)method_getTypeEncoding(method);
IMP originalImp = class_replaceMethod(originalClass, swizzledSelector, _objc_msgForward, typeDescription);

@onExit {
class_replaceMethod(originalClass, swizzledSelector, originalImp, typeDescription);
};

// Swizzle `forwardInvocation:` to intercept `swizzledSelector`.
id patchForwardInvocationBlock = ^(id self, NSInvocation *invocation) {
if (invocation.selector == swizzledSelector) {
expect(@(invoked)).to(beFalsy());
invoked = YES;
}
};

IMP newForwardInvocation = imp_implementationWithBlock(patchForwardInvocationBlock);
IMP oldForwardInvocation = class_replaceMethod(originalClass, @selector(forwardInvocation:), newForwardInvocation, "v@:@");

@onExit {
class_replaceMethod(originalClass, @selector(forwardInvocation:), oldForwardInvocation, "v@:@");
};

[object lifeIsGood:nil];
expect(@(invoked)).to(beTruthy());
});

qck_it(@"should invoke the swizzled `forwardInvocation:` on an instance isa-swizzled by RAC.", ^{
[object rac_signalForSelector:@selector(lifeIsGood:)];

SEL swizzledSelector = @selector(lifeIsGood:);

// Redirect `swizzledSelector` to the forwarding machinery.
Method method = class_getInstanceMethod(originalClass, swizzledSelector);
const char *typeEncoding = (char *)method_getTypeEncoding(method);
IMP originalImp = class_replaceMethod(originalClass, swizzledSelector, _objc_msgForward, typeEncoding);

@onExit {
class_replaceMethod(originalClass, swizzledSelector, originalImp, typeEncoding);
};

// Swizzle `forwardInvocation:` to intercept `swizzledSelector`.
id patchForwardInvocationBlock = ^(id self, NSInvocation *invocation) {
if (invocation.selector == swizzledSelector) {
expect(@(invoked)).to(beFalsy());
invoked = YES;
}
};

IMP newForwardInvocation = imp_implementationWithBlock(patchForwardInvocationBlock);
IMP oldForwardInvocation = class_replaceMethod(originalClass, @selector(forwardInvocation:), newForwardInvocation, "v@:@");

@onExit {
class_replaceMethod(originalClass, @selector(forwardInvocation:), oldForwardInvocation, "v@:@");
};

[object lifeIsGood:nil];
expect(@(invoked)).to(beTruthy());
});

qck_it(@"should invoke the swizzled selector on an instance isa-swizzled by RAC.", ^{
[object rac_signalForSelector:@selector(lifeIsGood:)];

SEL swizzledSelector = @selector(lifeIsGood:);

Method method = class_getInstanceMethod(originalClass, swizzledSelector);
const char *typeEncoding = (char *)method_getTypeEncoding(method);

id methodSwizzlingBlock = ^(id self) {
expect(@(invoked)).to(beFalsy());
invoked = YES;
};

IMP newImplementation = imp_implementationWithBlock(methodSwizzlingBlock);
IMP oldImplementation = class_replaceMethod(originalClass, swizzledSelector, newImplementation, typeEncoding);

@onExit {
class_replaceMethod(originalClass, swizzledSelector, oldImplementation, typeEncoding);
};

[object lifeIsGood:nil];
expect(@(invoked)).to(beTruthy());
});

qck_it(@"should invoke the swizzled setter on an instance isa-swizzled by RAC.", ^{
[object rac_signalForSelector:@selector(setObjectValue:)];

SEL swizzledSelector = @selector(lifeIsGood:);

Method method = class_getInstanceMethod(originalClass, swizzledSelector);
const char *typeEncoding = (char *)method_getTypeEncoding(method);

id methodSwizzlingBlock = ^(id self) {
expect(@(invoked)).to(beFalsy());
invoked = YES;
};

IMP newImplementation = imp_implementationWithBlock(methodSwizzlingBlock);
IMP oldImplementation = class_replaceMethod(originalClass, swizzledSelector, newImplementation, typeEncoding);

@onExit {
class_replaceMethod(originalClass, swizzledSelector, oldImplementation, typeEncoding);
};

[object lifeIsGood:nil];
expect(@(invoked)).to(beTruthy());
});
});

qck_it(@"should swizzle an NSObject method", ^{
NSObject *object = [[NSObject alloc] init];

Expand Down