4

Is there an Objective-C runtime library function (unlikely) or set of functions capable of inspecting static (quasi-class level) variables in Objective-C? I know I can utilize a class accessor method but I'd like to be able to test without writing my code "for the test framework".

Or, is there a obscure plain C technique for external access to static vars? Note this information is for unit testing purposes—it needn't be suitable for production use. I'm conscious that this'd go against the intent of static vars... a colleague broached this topic and I'm always interested in digging into ObjC/C internals.

@interface Foo : NSObject
+ (void)doSomething;
@end

@implementation Foo
static BOOL bar;
+ (void)doSomething
{
  //do something with bar
}
@end

Given the above can I use the runtime library or other C interface to inspect bar? Static variables are a C construct, perhaps there's specific zone of memory for static vars? I'm interested in other constructs that may simulate class variables in ObjC and can be tested as well.

james_womack
  • 10,028
  • 6
  • 55
  • 74
  • check `objc/runtime.h` – Bryan Chen Mar 27 '13 at 10:37
  • 1
    These variables come from C: the tags should not have been edited. – james_womack Mar 27 '13 at 10:39
  • I did check the runtime header and I use some of those functions quite often. – james_womack Mar 27 '13 at 10:40
  • If you want to use the Objective-C runtime to find that variable, use Objective-C to store it - not a plain C static var. – Jay Mar 27 '13 at 12:55
  • @Jay that's a logical assertion—however just because something comes from C doesn't necessarily mean we don't get enhanced access to it in the Objective-C runtime. ] knew it's unlikely I'd get a positive answer but as a pro you need to ask. *Although unlikely* I'd held out hope static variables (a C construct) got associated with a class in the ObjC runtime even though I knew it was unlikely. After all, instance vars in Objective-C are members on a struct that are auto-initialized to 0. Objective-C doesn't support class vars, despite the misleading `class_getInstanceVariable` function. – james_womack Mar 27 '13 at 18:01

2 Answers2

4

No, not really, unless you are exposing that static variable via some class method or other. You could provide a + (BOOL)validateBar method which does whatever checking you require and then call that from your test framework.

Also that isn't an Objective-C variable, but rather a C variable, so I doubt there is anything in the Objective-C Runtime that can help.

trojanfoe
  • 120,358
  • 21
  • 212
  • 242
  • +1, good reasoning on that isn't an Objective-C variable, but rather a C variable—I knew that but held onto the fantasy it may be possible through some loophole, and also hoped some of the old-school C guys would come up with a way to access it directly via a memory address or similar technique. – james_womack Mar 27 '13 at 22:04
2

The short answer is that accessing a static variable from another file isn't possible. This is exactly the same problem as trying to refer to a function-local variable from somewhere else; the name just isn't available. In C, there are three stages of "visibility" for objects*, which is referred to as "linkage": external (global), internal (restricted to a single "translation unit" -- loosely, a single file), and "no" (function-local). When you declare the variable as static, it's given internal linkage; no other file can access it by name. You have to make an accessor function of some kind to expose it.

The extended answer is that, since there is some ObjC runtime library trickery that we can do anyways to simulate class-level variables, we can make make somewhat generalized test-only code that you can conditionally compile. It's not particularly straightforward, though.

Before we even start, I will note that this still requires an individualized implementation of one method; there's no way around that because of the restrictions of linkage.

Step one, declare methods, one for set up and then a set for valueForKey:-like access:

//  ClassVariablesExposer.h

#if UNIT_TESTING
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

#define ASSOC_OBJ_BY_NAME(v) objc_setAssociatedObject(self, #v, v, OBJC_ASSOCIATION_ASSIGN)
// Store POD types by wrapping their address; then the getter can access the
// up-to-date value.
#define ASSOC_BOOL_BY_NAME(b) NSValue * val = [NSValue valueWithPointer:&b];\
objc_setAssociatedObject(self, #b, val, OBJC_ASSOCIATION_RETAIN)

@interface NSObject (ClassVariablesExposer)

+ (void)associateClassVariablesByName;

+ (id)classValueForName:(char *)name;
+ (BOOL)classBOOLForName:(char *)name;

@end
#endif /* UNIT_TESTING */

These methods semantically are more like a protocol than a category. The first method has to be overridden in every subclass because the variables you want to associate will of course be different, and because of the linkage problem. The actual call to objc_setAssociatedObject() where you refer to the variable must be in the file where the variable is declared.

Putting this method into a protocol, however, would require an extra header for your class, because although the implementation of the protocol method has to go in the main implementation file, ARC and your unit tests need to see the declaration that your class conforms to the protocol. Cumbersome. You can of course make this NSObject category conform to the protocol, but then you need a stub anyways to avoid an "incomplete implementation" warning. I did each of these things while developing this solution, and decided they were unnecessary.

The second set, the accessors, work very well as category methods because they just look like this:

//  ClassVariablesExposer.m

#import "ClassVariablesExposer.h"

#if UNIT_TESTING
@implementation NSObject (ClassVariablesExposer)

+ (void)associateClassVariablesByName
{
    // Stub to prevent warning about incomplete implementation.
}

+ (id)classValueForName:(char *)name
{
    return objc_getAssociatedObject(self, name);
}

+ (BOOL)classBOOLForName:(char *)name
{
    NSValue * v = [self classValueForName:name];
    BOOL * vp = [v pointerValue];
    return *vp;
}

@end
#endif /* UNIT_TESTING */

Completely general, though their successful use does depend on your employment of the macros from above.

Next, define your class, overriding that set up method to capture your class variables:

// Milliner.h

#import <Foundation/Foundation.h>

@interface Milliner : NSObject
// Just for demonstration that the BOOL storage works.
+ (void)flipWaterproof;
@end

// Milliner.m

#import "Milliner.h"

#if UNIT_TESTING
#import "ClassVariablesExposer.h"
#endif /* UNIT_TESTING */

@implementation Milliner
static NSString * featherType;
static BOOL waterproof;

+(void)initialize
{
    featherType = @"chicken hawk";
    waterproof = YES;
}

// Just for demonstration that the BOOL storage works.
+ (void)flipWaterproof
{
    waterproof = !waterproof;
}

#if UNIT_TESTING
+ (void)associateClassVariablesByName
{
    ASSOC_OBJ_BY_NAME(featherType);
    ASSOC_BOOL_BY_NAME(waterproof);
}
#endif /* UNIT_TESTING */

@end

Make sure that your unit test file imports the header for the category. A simple demonstration of this functionality:

#import <Foundation/Foundation.h>
#import "Milliner.h"
#import "ClassVariablesExposer.h"

#define BOOLToNSString(b) (b) ? @"YES" : @"NO"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        [Milliner associateClassVariablesByName];
        NSString * actualFeatherType = [Milliner classValueForName:"featherType"];
        NSLog(@"Assert [[Milliner featherType] isEqualToString:@\"chicken hawk\"]: %@", BOOLToNSString([actualFeatherType isEqualToString:@"chicken hawk"]));

        // Since we got a pointer to the BOOL, this does track its value.
        NSLog(@"%@", BOOLToNSString([Milliner classBOOLForName:"waterproof"]));
        [Milliner flipWaterproof];
        NSLog(@"%@", BOOLToNSString([Milliner classBOOLForName:"waterproof"]));

    }
    return 0;
}

I've put the project up on GitHub: https://github.com/woolsweater/ExposingClassVariablesForTesting

One further caveat is that each POD type you want to be able to access will require its own method: classIntForName:, classCharForName:, etc.

Although this works and I always enjoy monkeying around with ObjC, I think it may simply be too clever by half; if you've only got one or two of these class variables, the simplest proposition is just to conditionally compile accessors for them (make an Xcode code snippet). My code here will probably only save you time and effort if you've got lots of variables in one class.

Still, maybe you can get some use out of it. I hope it was a fun read, at least.


*Meaning just "thing that is known to the linker" -- function, variable, structure, etc. -- not in the ObjC or C++ senses.

Community
  • 1
  • 1
jscs
  • 63,694
  • 13
  • 151
  • 195
  • 1
    I love your answers. They are always so relevant. But maybe this is not appropriate as a comment, is it? This answer is really helpful for me. Thank you. –  Apr 07 '13 at 19:21
  • Thanks for your appreciation, @IsaMeg! It looks like you may have just upvoted several of my answers in quick succession. The thought is nice, but you should be aware that the system [might consider that fradulent voting](http://meta.stackexchange.com/questions/126829/what-is-serial-voting-and-how-does-it-affect-me) and reverse the votes automatically. I'm glad that you find my posts helpful, however. – jscs Apr 07 '13 at 19:28
  • I upvote nice answers from people who post nice answers. Sounds right. –  Apr 07 '13 at 20:48
  • I don't want to dissuade you from upvoting good answers, just to make you aware of the scrutiny applied to upvotes on a bunch of posts by the same person in a very short time. – jscs Apr 07 '13 at 20:58