Shakespeare said something about roses and names smelling nice, but if you’re Outkast you know that sometimes, roses really smell like poo. Additionally, if you’re a programmer, you sometimes need to get another set of better spelling roses to set the world right. Far-fetched metaphors aside, namespacing issues come up less than I would expect. For one, most people know better than to call their classes ‘Object’, and everyone has their own novel solutions to the problem – be it prefixing every class with their initials or a swear word. This week I came across a project that required two implementations of the same framework to be present in the same iOS app. The frameworks had the same, or very similar headers, but the implementations were quite different. The use case in question was for migrating data from one implementation of the framework to another. Objective-C doesn’t have the namespace keyword magic that would make this problem a little faster.
My thought process evolved like this:
- My first thought was to rename everything in the old framework by adding a new ‘OLD’ prefix. However, this is quite a lot of work, depending on the size of the framework, and it can be tricky to come up with the right sed command that does everything you wanted.
- My next thought was to use #defines for everything I wanted to namespace in the headers only. However, this poses some serious problems for class naming conventions, even if you only conditionally define it within the framework compilation unit. Consider a class called ‘MyClass’. You might have MyClassDelegate, eTargetTypeMyClass, and even other classes with the same prefix or suffix, like MyClassManager or ComplicatedMyClass. #defines might or might not expand in the way you want here – but you can see the risk: it’s easy to end up with half of the file containing ComplicatedOLDMyClass in one compilation unit and OLDComplicatedMyClass in the other.
- 3. I found out about @compatibility_alias, which solves the problems with #2 for classes (and nothing else). However, this actually solved most of my problems and was fairly compatible with doing non-class namespacing as mentioned in 2.
@interface OLDMyClass
...
@end
@protocol OLDMyProtocol
@end
#ifdef COMPILING_MYCLASS
@compatibility_alias MyClass OLDMyClass; // this tricks the MyClass.m file to use the OLDMyClass interface
#define MyProtocol OLDMyProtocol
#endif
Limitations of @compatibility_alias:
- You can only use it with a real class to an aliased class. You can use it before the real class is declared (but the compiler will emit a warning). If the alias class is already declared, it will cause an error.
- You can only use it once per compilation unit for a given alias (you can’t use it liberally like you can with forward declares).
- You cannot use it with protocols, typdefs, enums, or anything besides an objective-c class.
- All @class MyClass forward decl’s will need to either be swapped out for an #import “MyClass.h” or changing the forward decl to @class OLDMyClass and all occurrances in the header to OLDMyClass.
So how do you namespace the protocols, typedefs, and enums? You could rename everything in the .h and .m files, but it is a lot less work to just ‘#define originalprotocol OLDoriginalprotocol’ and declare OLDProtocol in the header.
You can then #ifdef the #defines and @compatibility_alias’s on the condition that you are compiling the framework itself. This trick is similar to exporting only certain symbols when building a library, and allows for the framework’s implementation (.m files) to be remain almost entirely unchanged (using the ‘originalclass’ form), and when compiling the app, the app only sees the OLD_originalclass form, and also the #defines won’t be able to accidentally expand strings that it happens to match.
I ended up using a sed command only to change the #import to #import.
I did the above for the older version of the framework, and left the newer one unchanged. This allows them to both be used simultaneously. There are other tricks you might be able to do like dynamic loading, but if you need classes from both at the same time, this is probably the way to go.
For reference, here’s a file that I used to test the edge cases. You can compile with ‘clang -framework Foundation compatibility_alias_test.m’, or see the errors caused by wrong usage with ‘clang -DWONT_COMPILE -framework Foundation compatibility_alias_test.m’ :
#include <Foundation/Foundation.h>
#include <stdio.h>
// 1.Only classes work with @compatibility_alias:
// 2. Protocols, typedef, enums and other things are out.
// 3. You might be able to use a #define with #undef for those
// 3.1. If that would interfere with the original namespace you are colliding with,
// you might try conditionally defining it where it is needed, probably similar
// to exporting only some symbols when building a lib (if just including headers it would be hidden)
#ifdef WONT_COMPILE
// forward declare of aliased name conflicts with alias
@class SpecificClass; // error: conflicting types for alias 'SpecificClass'
#endif
@protocol NonColiding_BeerHolder
@property (nonatomic, assign) int beers;
@end
#ifdef WONT_COMPILE
@compatibility_alias BeerHolder NonColiding_BeerHolder; // warning: cannot find interface declaration for 'NonColiding_BeerHolder'
// also, later: error: cannot find protocol declaration for 'BeerHolder'
#else
#define BeerHolder NonColiding_BeerHolder
#endif
@interface NonColiding_SpecificClass : NSObject <BeerHolder>
@property (nonatomic, assign) int beers;
@end
@implementation NonColiding_SpecificClass
- (id)initWithBeers:(int)beers
{
if ((self = [super init])) {
self.beers = beers;
}
return self;
}
@end
@compatibility_alias SpecificClass NonColiding_SpecificClass;
#ifdef WONT_COMPILE
// you also can't double up on compatibility_alias like you can with forward decls.
@compatibility_alias SpecificClass NonColiding_SpecificClass;
#endif
int main(int argc, const char* argv[])
{
SpecificClass* bob = [[SpecificClass alloc] initWithBeers:5];
NSObject<BeerHolder>* bh = bob;
// sanity
printf("yo dog i'm bob and i have %d beers.\n", bob.beers);
printf("yo dog i'm a beer holder and i have %d beers.\n", bh.beers);
[bob release];
}