NSProxy pretending to be Class doesn't handle respondsToSelector in 64-bit runtime
Asked Answered
S

1

7

In OCMockito, test doubles are implemented with NSProxy. A double standing in for an instance implements -respondsToSelector: as follows:

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [_mockedClass instancesRespondToSelector:aSelector];
}

But a double standing in for a class implements -respondsToSelector: like this:

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [_mockedClass respondsToSelector:aSelector];
}

This all works in the 32-bit runtime. For example, if _mockedClass is [NSString class], the proxy correctly answers that it responds to the selector +pathWithComponents:

But in the 64-bit runtime, it crashes:

Crashed Thread:  0  Dispatch queue: com.apple.main-thread

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: EXC_I386_GPFLT

Application Specific Information:
objc[1868]: GC: forcing GC OFF because OBJC_DISABLE_GC is set

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   libobjc.A.dylib                 0x00007fff95cbffc6 cache_getImp + 6
1   libobjc.A.dylib                 0x00007fff95ccd1dc lookUpImpOrForward + 50
2   libobjc.A.dylib                 0x00007fff95ccd198 lookUpImpOrNil + 20
3   libobjc.A.dylib                 0x00007fff95cc218a class_respondsToSelector + 37
4   com.apple.CoreFoundation        0x00007fff91c131ad ___forwarding___ + 429
5   com.apple.CoreFoundation        0x00007fff91c12f78 _CF_forwarding_prep_0 + 120
6   org.mockito.OCMockitoTests      0x000000010451a55b -[StubClassTest testStubbedMethod_ShouldReturnGivenObject] + 107 (StubClassTest.m:48)

Note that it's calling class_respondsToSelector(…). I suspect that I'm being bitten by an optimization made to the runtime. What can I do to fix this?

Soulier answered 25/5, 2014 at 17:7 Comment(6)
Summoned Greg Parker. I don't have enough of a clue to know from the evidence given. That looks like a corrupt isa?Strutting
@Strutting The thing is, I'm not touching isa. The failing test is checked in to github.com/jonreid/OCMockitoSoulier
Do you have a sample project, btw?Strutting
Beats me. Sample project and crash log might help.Curvy
I've boiled down a sample: qualitycoding.org/RuntimeProblem.zip Run the unit tests and you should see a crash in StubClassTest's testStubbedMethod_ShouldReturnGivenObject. But to make things even more fun, disable the ArgumentCaptorTest… and the formerly crashing test passes.Soulier
One interesting thing is this: if you put a breakpoint in : - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector it no longer crashes.Rajkot
B
7

it's a bit long answer, so bear with me. I ran a simple code just to verify the behavior:

Class mock = mockClass([NSProcessInfo class]);
[mock processInfo];
[verify(mock) processInfo];

Indeed It does crash with bad pointer exception. Replacing first line with

id mock = mockClass([NSProcessInfo class]);

works as expected. I figured that it might be worth to look at the code after ARC. Those snippets are a bit to long, so here are the gists: Class-based test, id-based test

As you can see, when you declare variable of type Class there is an extra release. My guess is that since classes are registered for the entire runtime duration (unless removed using runtime api) it's ok to have Class variable as __unsafe_unretained.

To summarize, you have two possible solutions:

@implementation StubClassTest
{
    __strong Class mockClass;
}

or

@implementation StubClassTest
{
    id mockClass;
}

seem to fix the issue for me.

Update

As a special case, if the object’s base type is Class (possibly protocol-qualified), the type is adjusted to have __unsafe_unretained qualification instead.

From http://clang.llvm.org/docs/AutomaticReferenceCounting.html#objects

Botts answered 28/5, 2014 at 13:23 Comment(5)
Thank you so much!! I've assumed all this time that Class ivars (being objects in my mind) were __strong by default. How does one see that intermediate code with retains & releases?Soulier
Glad it helped, sadly, it doesn't explain why this code works on 32bit os and doesn't work on 64, maybe sir Bill Bumgarner or Greg Parker can shed some light on this matter.Botts
On Intel, the 32 bit runtime is the legacy runtime whereas the 64 bit runtime is modern (like iOS). The difference lies within, I'd guess.Strutting
32-bit OS X doesn't support ARC. Presumably your 32-bit manual memory management is retaining the proxy object even though its compile-time type is Class.Curvy
That makes sense. What about iOS? Both 32 and 64 bit are ARC-enabled in this caseBotts

© 2022 - 2024 — McMap. All rights reserved.