├── .gitignore ├── README.mdown ├── RMModelObject.h ├── RMModelObject.mm ├── RMModelObject.xcodeproj └── project.pbxproj ├── RMModelObjectTest-Info.plist └── RMModelObjectTest.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | 17 | ## Ignore incredibly annoying .DS_Store files 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | RMModelObject 2 | ============= 3 | 4 | RMModelObject has been superceded by Mantle 5 | ------------------------------------------- 6 | 7 | RMModelObject was written a long time ago by Objective-C standards, 8 | back in ~2006 when the iPhone didn't even exist. It was written for 9 | the Objective-C 1.0 32-bit runtime, and will not work properly for 10 | Objective-C 2.0. 11 | 12 | If you're looking for something with RMModelObject-like functionality 13 | that works on modern Objective-C runtimes, check out 14 | [Mantle](https://github.com/github/Mantle) and MTLModel! It basically 15 | does the same thing as RMModelObject, but works with today's far 16 | better Objective-C runtimes. 17 | 18 | On the other hand, if you _are_ stuck with the Objective-C 1.0 32-bit 19 | runtime for whatever reason, RMModelObject may still be useful for you. 20 | 21 | 22 | Introduction 23 | ------------ 24 | 25 | _RMModelObject_ is a class that makes writing model objects and [value 26 | objects](http://c2.com/cgi/wiki?ValueObject) in Cocoa a lot easier. Let's walk 27 | through a short example: say you're writing a new blogging program, and want 28 | a class that represents a blog entry. So, you start by writing a class that 29 | looks like this: 30 | 31 | @interface MyBlogEntry : NSObject 32 | { 33 | NSString* title; 34 | NSCalendarDate* postedDate; 35 | NSAttributedString* bodyText; 36 | NSArray* tagNames; 37 | NSArray* categoryNames; 38 | BOOL isDraft; 39 | } 40 | 41 | @property (copy) NSString* title; 42 | @property (copy) NSCalendarDate* postedDate; 43 | @property (copy) NSAttributedString* bodyText; 44 | @property (copy) NSArray* tagNames; 45 | @property (copy) NSArray* categoryNames; 46 | @property BOOL isDraft; 47 | 48 | @end 49 | 50 | Then you remember that Apple had written some [documentation about model 51 | objects](http://developer.apple.com/documentation/Cocoa/Conceptual/ModelObjects/) 52 | and go to read it, and become sad as you realise that you then really should write the following methods: 53 | 54 | * The accessors: `-title`, `-setTitle:`, `-postedDate`, `-setPostedDate:`, 55 | `-bodyText`, `-setBodyText:`, `-tagNames`, `-setTagNames:`, `-categoryNames`, 56 | `-setCategoryNames:`, `-isDraft`, `-setIsDraft:`. 57 | * `-isEqual:`, so that you can actually compare blog entries. 58 | * `-hash`, because you just wrote `-isEqual:`. 59 | * `-copyWithZone:`, because you'd like to duplicate the blog entry. 60 | * `-encodeWithCoder:` and `-initWithCoder:`, because `NSCoding` support is 61 | pretty handy for being able to, say, copy'n'paste the thing to the clipboard. 62 | Don't forget to support both keyed and unkeyed archiving! 63 | * `-dealloc`, to tidy up nicely. 64 | 65 | Not so much fun now, is it? Not only is that a lot of extra code to write, but 66 | you've done it (maybe literally) hundreds of times before. It's really boring 67 | code to write, and you should probably go write a test suite for it even though 68 | that's even more boring. 69 | 70 | Objective-C 2.0 makes at least the accessor pain go away by giving you 71 | `@synthesize`, but that's it. If you're truly a modern runtime ninja and 72 | cutting 64-bit Objective-C 2.0 code, you can even get away without declaring 73 | those pesky instance variables, but again, you still have to write the other 74 | six methods. 75 | 76 | However, now that you've encountered RMModelObject, what's what you need to do 77 | to write a full-fledged model object with full support for `NSCopying` and 78 | `NSCoding`: 79 | 80 | @interface MyBlogEntry : RMModelObject 81 | 82 | @property (copy) NSString* title; 83 | @property (copy) NSCalendarDate* postedDate; 84 | @property (copy) NSAttributedString* bodyText; 85 | @property (copy) NSArray* tagNames; 86 | @property (copy) NSArray* categoryNames; 87 | @property BOOL isDraft; 88 | 89 | @end 90 | 91 | // 92 | 93 | @implementation MyBlogEntry 94 | 95 | @dynamic title, postedDate, bodyText, tagNames, categoryNames, isDraft; 96 | 97 | @end 98 | 99 | That's it. (And you only need the @dynamic to suppress a compiler warning, 100 | hmpf.) The RMModelObject superclass does the rest. In summary, RMModelObject 101 | means: 102 | 103 | * no need to declare instance variables, 104 | * no need to write accessor methods, 105 | * free NSCopying protocol support (`-copyWithZone:`), 106 | * free NSCoding protocol support (`-initWithCoder:`, `-encodeWithCoder:`), 107 | * free `-isEqual:` and -hash` implementation, 108 | * no need to write `-dealloc` in most cases. 109 | 110 | There's one handy extra feature: your class can adopt the 111 | _RMModelObjectPropertyChanging_ protocol, which means that it can receive 112 | callbacks when properties are changed, both before and after the change. 113 | Think of this as a poor man's KVO. Here's the two methods that you want 114 | to implement: 115 | 116 | @protocol RMModelObjectPropertyChanging 117 | 118 | @optional 119 | 120 | - (BOOL)propertyWillChange:(NSString*)propertyName from:(id)oldValue to:(id)newValue; 121 | - (void)propertyDidChange:(NSString*)propertyName from:(id)oldValue to:(id)newValue; 122 | 123 | @end 124 | 125 | Hopefully the method names are explanatory enough; see the comments in 126 | RMModelObject.h for more information. 127 | 128 | There's also a couple of other nice characteristics of RMModelObject that make 129 | it appealing: 130 | 131 | * The RMModelObject base class does not have any extra instance variables, so 132 | it doesn't change the memory layout of your class and therefore retains full 133 | ABI compatibility, making it easy to try out. 134 | * For the 32-bit runtime, since you're not using instance variables in your 135 | class, you are not susceptible to the [fragile base 136 | class](http://cocoacafe.wordpress.com/2007/06/20/the-fragile-base-class-problem/) 137 | problem. 138 | 139 | You can probably stop reading here and start playing right now, that's 140 | really all you need to know about it. Hopefully you'll find it useful! 141 | The rest of this document is a high-level overview about how those six 142 | main features work. 143 | 144 | 145 | Known Issues 146 | ------------ 147 | 148 | * On Mac OS X you will have to link against “ApplicationServices.framework”. 149 | * There currently is no support for compiling with ARC enabled. 150 | 151 | 152 | Implementation Overview 153 | ----------------------- 154 | 155 | RMModelObject uses a lot of the [Objective-C 2.0 156 | runtime](http://developer.apple.com/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html) 157 | facilities to do its voodoo. If you've written a RMModelObject subclass named 158 | `Foo`, RMModelObject essentially [introspects the 159 | properties](http://developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC/Articles/chapter_5_section_6.html) 160 | of Foo, and uses that introspected data to dynamically create a new class at 161 | runtime that has a backing store for those properties with appropriate getter and setter 162 | methods. 163 | 164 | By overriding `+allocWithZone:`, we make the instances of your class to really 165 | be instances of this dynamically generated class, which is named 166 | `RMModelObject_Foo`. So, the final class hierarchy for an instance of the 167 | `Foo` class looks like 168 | 169 | RMModelObject implements NSCopying, NSCoding, -isEqual: & hash 170 | ^ 171 | | 172 | Foo 173 | ^ 174 | | 175 | RMModelObject_Foo implements accessors and backing store 176 | 177 | So, RMModelObject_Foo inherits from Foo, which inherits from RMModelObject. 178 | There's a division of responsibility here: the `RMModelObject_*` classes 179 | implement the accessor methods that are specific to a particular class, which 180 | means that they are also somehow responsible for implementing the storage 181 | (backing store) for the accessors. Probably the most complex logic in the 182 | RMModelObject implementation is allocating and registering the dynamically 183 | generated class, while the most tricky (but not too complex) code is the actual 184 | accessor methods themselves, because there must be one getter and setter for 185 | each primitive C type. Objective-C lets you dynamically add methods to 186 | a class, but those methods require implementations, and those implementations 187 | aren't generated dynamically generated. (Yet!) 188 | 189 | The RMModelObject base class then handles the methods that are common to all 190 | model objects, which are `-copyWithZone:`, `-encodeWithCoder:`, 191 | `-initWithCoder:`, `isEqual:` and `hash`. Since `self` in Objective-C always 192 | points to the most-derived class, it's no problem for the RMModelObject base 193 | class to introspect its subclass's properties. 194 | 195 | 196 | Property Access & Performance 197 | ----------------------------- 198 | 199 | Each property that you declare obviously has to have its value stored 200 | somewhere in memory. The main goal that I had for this was performance: 201 | property access had to be fast enough so that you wouldn't have to think 202 | about whether using RMModelObject would slow you down or not. 203 | 204 | This implementation is actually in its third generation: I'd rewritten 205 | RMModelObject twice before with all the same capabilities, but performance 206 | was slow enough in the first two versions that accessing properties in 207 | tight loops took up a very significant amount of time in a Shark profile. 208 | The very first version used a simple NSDictionary ivar to store all the 209 | property values. This worked fine, but killed performance because you 210 | suffered two more levels of indirection: one pointer dereference from the 211 | model object to the dictionary, and another pointer dereference from the 212 | dictionary to the value. Additionally, if the property was a primitive 213 | value (e.g. an `unsigned int` or `BOOL`), which is very common for many 214 | value objets, there was the extra overhead of wrapping and unwrapping that 215 | value in an `NSNumber` or `NSValue` structure. The performance impact was 216 | unacceptable. 217 | 218 | The second version used a C++ STL `std::map` with a high-performance 219 | [dynamic-typing value 220 | store](http://www.codeproject.com/KB/cpp/dynamic_typing.aspx) to improve 221 | speed: small values such as pointers and integers could be stored directly 222 | in the map, eliminating one pointer dereference. Unfortunately, since STL 223 | maps are typically red-black tree implementations, there was still 224 | a significant performance overhead due to pointer dereferencing. 225 | Additionally, the implementation was very complex: trying to intermix 226 | Objective-C's dynamic typing with C++ templates while trying to maintain 227 | correct assign/retain/copy semantics for properties was very difficult. 228 | 229 | Ideally, what I was after was a single contiguous area of memory: a simple 230 | array that I could index into at will, just like the classic ivar layout 231 | of a class or struct. We could pre-declare a reasonable-sized fixed-size 232 | array to play with, but Since RMModelObject had to be flexible enough to 233 | accommodate model objects big and small, that array would have to grow for 234 | large objects, and could waste a lot of memory for small objects, so 235 | I didn't find that solution acceptable. 236 | 237 | Then, duh, I remmbered that the Objective-C runtime lets you dynamically 238 | create a class, and one can add instance variables to it and even get 239 | their offsets using a name. Perfect! So, the current implementation 240 | simply uses the Objective-C runtime to do all the heavy lifting. This 241 | gives us a few nice features. First, if the Objective-C runtime improves 242 | its dynamic ivar access speed in the future, RMModelObject will benefit 243 | from it. Second, our implementation becomes simpler since we can push all 244 | that work down the stack. Third, I'm reasonably sure that this is 245 | more-or-less how the 64-bit runtime works for its non-fragile ivar 246 | support, so we can claim to be around the same performance as that, which 247 | not only perfectly acceptable, it means that we're comparable in speed to 248 | what will hopefully be the standard way of doing things in the future. 249 | 250 | 251 | Generated Accessor Methods 252 | -------------------------- 253 | 254 | RMModelObject generates a getter and setter method for all primitive 255 | types, as well the four main struct types used in Cocoa 256 | (`NSRect`/`CGRect`, `NSPoint`/`CGPoint`, `NSSize`/`CGSize`, and 257 | `NSRange`). Right now, there's no way to extend that list besides hacking 258 | the source code, though future versions of RMModelObject will have an API 259 | to extend this. One pair of getter and setter methods are generated for 260 | each property. The getter/setter calls into a C++ template function 261 | (templatised over the property name) to do the actual setting of the 262 | instance variable. Due to the Objective-C runtime having buggy 263 | implementations of `object_getIvar()` and `object_setIvar()`, the accessor 264 | methods calculate the ivar offset with `ivar_getOffset()` for the 265 | read/write, and read directly from that offset. For `id` (object) types, 266 | we use `objc_assign_ivar()` so that garbage collection is properly 267 | supported. 268 | 269 | For each setter method, there's also a "slow" version and a "fast" 270 | version. When the dynamic class is registered, RMModelObject will check 271 | whether your class implements the `-propertyWillChange:from:to:` or 272 | `-propertyDidChange:from:to:` methods. If it implements either of those 273 | methods, the slow version is used; if not, the fast version is used. 274 | 275 | 276 | TODO 277 | ---- 278 | 279 | There's certainly a lot of improvement that can be made to RMModelObject. 280 | * Support for __weak instance variables 281 | * API to extend support for various types 282 | * "Functional programming style" updater setter methods, that return a new 283 | copy of the current object. (e.g. -updatingFoo: rather than -setFoo:). 284 | * Copy-on-write support 285 | * Automatic undo support 286 | * Custom setter/getter names for property 287 | * Properly support atomic/nonatomic properties 288 | * Extend unit tests to cover all types 289 | * Support copying/archiving of pointer types (void*/char*), perhaps via a delegate method 290 | * General code organisation: it's pretty untidy right now. 291 | 292 | 293 | Closing Remarks 294 | --------------- 295 | 296 | I've found RMModelObject to be tremendously useful so far: it's already 297 | been used for well over a dozen classes so far and has cut down on both 298 | a ton of tedious work and development time, making it trivial to create 299 | proper model objects instead of using cheap hacks, such as a struct or an 300 | NSDictionary with named keys. (The latter provides you with no 301 | encapsulation.) One of our large model object classes had over 302 | 1000 lines eliminated thanks to the `-propertyWillChange:from:to:` and 303 | `-propertyDidChange:from:to:` methods. Additionally, since all the logic 304 | to perform the copying and archiving of model objects is contained in one 305 | place, it's also cut down on the number of bugs. 306 | 307 | I hope it's useful to you, and if you feel like contributing back to it, 308 | please feel free to contact me for write access to the Realmac Forge 309 | Subversion repository, I'm very happy to give out commit bits. (If 310 | there's enough demand, maybe I'll move it to github some day.) 311 | 312 | > [Andre Pang](andre.pang@realmacsoftware.com) 313 | 314 | /* vim: set sts=4 expandtab */ 315 | -------------------------------------------------------------------------------- /RMModelObject.h: -------------------------------------------------------------------------------- 1 | //*************************************************************************** 2 | 3 | // Copyright (C) 2008 Realmac Software Ltd 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject 11 | // to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | // ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 21 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | //*************************************************************************** 25 | 26 | #import 27 | 28 | //*************************************************************************** 29 | 30 | /** See http://developer.apple.com/documentation/Cocoa/Conceptual/ModelObjects/ */ 31 | @interface RMModelObject : NSObject 32 | 33 | - (BOOL)isEqualToModelObject:(RMModelObject*)other; 34 | 35 | @end 36 | 37 | //*************************************************************************** 38 | 39 | @protocol RMModelObjectPropertyChanging 40 | 41 | @optional 42 | 43 | /// Called when the given property name is about to be changed. Return YES to accept the change, or NO to reject the change. If this method is not implemented, it is assumed that all property changes will be accepted. 44 | /** Primitive value types are marshalled into NSNumber/NSValue objects. */ 45 | - (BOOL)propertyWillChange:(NSString*)propertyName from:(id)oldValue to:(id)newValue; 46 | 47 | /// Called directly after the given property name has changed. 48 | /** Primitive value types are marshalled into NSNumber/NSValue objects. */ 49 | - (void)propertyDidChange:(NSString*)propertyName from:(id)oldValue to:(id)newValue; 50 | 51 | @end 52 | 53 | //*************************************************************************** 54 | 55 | /// Registers a new RMModelObject_* dynamic class based on the given class. 56 | Class RMModelObjectInitializeDynamicClass(Class mainClass); 57 | 58 | //*************************************************************************** 59 | -------------------------------------------------------------------------------- /RMModelObject.mm: -------------------------------------------------------------------------------- 1 | //*************************************************************************** 2 | 3 | // Copyright (C) 2008 Realmac Software Ltd 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject 11 | // to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | // ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 21 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | //*************************************************************************** 25 | 26 | #import "RMModelObject.h" 27 | 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | 34 | //*************************************************************************** 35 | 36 | BOOL RMModelObjectDebuggingEnabled = NO; 37 | 38 | #define MOLog(args...) { if(RMModelObjectDebuggingEnabled) NSLog(args); } 39 | 40 | //*************************************************************************** 41 | 42 | #if !defined(TARGET_OS_IPHONE) || TARGET_OS_IPHONE == 0 43 | static inline NSString* NSStringFromCGRect(const CGRect rect) 44 | { 45 | return NSStringFromRect(NSRectFromCGRect(rect)); 46 | } 47 | 48 | static inline NSString* NSStringFromCGSize(const CGSize size) 49 | { 50 | return NSStringFromSize(NSSizeFromCGSize(size)); 51 | } 52 | 53 | static inline NSString* NSStringFromCGPoint(const CGPoint point) 54 | { 55 | return NSStringFromPoint(NSPointFromCGPoint(point)); 56 | } 57 | #endif 58 | 59 | //*************************************************************************** 60 | 61 | typedef char ObjCPropertyAssignmentMode; 62 | static const char ObjCPropertyAssignmentModeAssign = '\0'; 63 | static const char ObjCPropertyAssignmentModeRetain = '&'; 64 | static const char ObjCPropertyAssignmentModeCopy = 'C'; 65 | 66 | //*************************************************************************** 67 | 68 | // TODO: get rid of this InSituCString class, it's kinda useless 69 | struct InSituCString 70 | { 71 | const char* cString; 72 | unsigned length; // specifically not size_t, to conserve space (this struct really isn't meant for strings that could be 64 bits long...) 73 | 74 | inline InSituCString() 75 | : cString(NULL) 76 | { 77 | } 78 | 79 | inline InSituCString(const char* aCString) 80 | : cString(aCString) 81 | , length(strlen(aCString)) 82 | { 83 | } 84 | 85 | inline InSituCString(const char* aCString, const unsigned aLength) 86 | : cString(aCString) 87 | , length(aLength) 88 | { 89 | } 90 | 91 | inline std::string String() const 92 | { 93 | return std::string(cString, length); 94 | } 95 | }; 96 | 97 | //--------------------------------------------------------------------------- 98 | 99 | // TODO: Maybe convert this to a standard C struct and have a ObjCPropertyAttributesMake() function, it's probably a lot more obvious for Cocoa folks 100 | struct ObjCPropertyAttributes 101 | { 102 | ObjCPropertyAssignmentMode assignmentMode; 103 | BOOL isReadOnly; 104 | BOOL isWeak; 105 | BOOL isDynamic; 106 | BOOL isSynthesized; 107 | std::string typeEncoding; 108 | InSituCString customGetterName; 109 | InSituCString customSetterName; 110 | 111 | inline ObjCPropertyAttributes() 112 | : assignmentMode(ObjCPropertyAssignmentModeAssign) 113 | , isReadOnly(NO) 114 | , isWeak(NO) 115 | , isDynamic(NO) 116 | , isSynthesized(NO) 117 | , typeEncoding() 118 | , customGetterName() 119 | , customSetterName() 120 | { 121 | } 122 | 123 | inline ObjCPropertyAttributes(const char* const propertyAttributesCString) 124 | { 125 | isReadOnly = NO; 126 | isWeak = NO; 127 | isDynamic = NO; 128 | isSynthesized = NO; 129 | 130 | assignmentMode = ObjCPropertyAssignmentModeAssign; 131 | 132 | const char* p = propertyAttributesCString; 133 | const char* const pEnd = p+strlen(propertyAttributesCString); 134 | 135 | for(; p < pEnd; p++) 136 | { 137 | const char c = *p; 138 | 139 | switch(c) 140 | { 141 | case 'R': 142 | isReadOnly = YES; 143 | break; 144 | case 'T': 145 | { 146 | p++; 147 | const char* start = p; 148 | size_t length = 0; 149 | while(p < pEnd && *p != ',') 150 | { 151 | length++; 152 | p++; 153 | } 154 | typeEncoding = std::string(start, length); 155 | break; 156 | } 157 | case ObjCPropertyAssignmentModeCopy: 158 | case ObjCPropertyAssignmentModeRetain: 159 | NSCAssert2(assignmentMode == ObjCPropertyAssignmentModeAssign, 160 | @"Found %c property attribute when we've already seen an assignment mode of %c", 161 | c, assignmentMode); 162 | assignmentMode = c; 163 | break; 164 | case 'D': // Dynamic 165 | isDynamic = YES; 166 | break; 167 | case 'V': // Synthesized 168 | isSynthesized = YES; 169 | while(*p != ',') p++; 170 | break; 171 | case 'P': // Strong reference 172 | break; 173 | case 'W': // Weak reference 174 | isWeak = YES; 175 | break; 176 | case ',': // Property separator 177 | break; 178 | // default: 179 | // NSCAssert2(NO, @"Encountered unknown property attribute character: %c (%s)", c, propertyAttributesCString); 180 | } 181 | } 182 | } 183 | }; 184 | 185 | //*************************************************************************** 186 | 187 | // We need these NewVariableName and NewVariableNameInner macros to generate a unique identifier for each enumerator. 188 | #define NewVariableNameInner(name, line) name ## line 189 | // The NewVariableName macro is needed even though it appears to not do anything, due to the how the C pre-processor works. 190 | #define NewVariableName(name, line) NewVariableNameInner(name, line) 191 | 192 | /// Small macro to ease the burden of writing a for() loop to enumerate over all the instance variables in a class. The first parameter is the name of the newly created ivar, and the second parameter is the object to check (typically just "ivar", and "self", respectively). 193 | #define FOR_ALL_IVARS(ivar, self) \ 194 | unsigned int NewVariableName(numberOfIvars, __LINE__) = 0; \ 195 | const Ivar* const NewVariableName(ivars, __LINE__) = class_copyIvarList([self class], &NewVariableName(numberOfIvars, __LINE__)); \ 196 | void *free_me_using_FREE_FOR_ALL = (void *)NewVariableName(ivars, __LINE__); \ 197 | NSUInteger NewVariableName(i, __LINE__) = 0; \ 198 | for(Ivar ivar = NewVariableName(ivars, __LINE__)[NewVariableName(i, __LINE__)]; \ 199 | NewVariableName(i, __LINE__) < NewVariableName(numberOfIvars, __LINE__); \ 200 | ivar = NewVariableName(ivars, __LINE__)[++NewVariableName(i, __LINE__)]) 201 | 202 | #define FREE_FOR_ALL free(free_me_using_FREE_FOR_ALL) 203 | 204 | //*************************************************************************** 205 | 206 | template static inline T* IvarLocation(id self, Ivar const ivar) 207 | { 208 | return reinterpret_cast( (char*)self+ivar_getOffset(ivar) ); 209 | } 210 | 211 | template static inline void GetInstanceVariable(id self, Ivar const ivar, T* const pValue) 212 | { 213 | *pValue = *IvarLocation(self, ivar); 214 | } 215 | 216 | template static inline void GetInstanceVariable(id self, const char* const ivarName, T* const pValue) 217 | { 218 | GetInstanceVariable(self, class_getInstanceVariable([self class], ivarName), pValue); 219 | } 220 | 221 | template static inline void SetInstanceVariable(id self, Ivar const ivar, const T* const pValue) 222 | { 223 | *IvarLocation(self, ivar) = *pValue; 224 | } 225 | 226 | template static inline void SetInstanceVariable(id self, const char* const ivarName, const T* const pValue) 227 | { 228 | SetInstanceVariable(self, class_getInstanceVariable([self class], ivarName), pValue); 229 | } 230 | 231 | template static inline T GetRawValue(id self, SEL _cmd) 232 | { 233 | T value; 234 | GetInstanceVariable(self, sel_getName(_cmd), &value); 235 | 236 | return value; 237 | } 238 | 239 | static inline const char* const PropertyNameFromSetterName(Class aClass, SEL setterMethodName) 240 | { 241 | const char* setterName = sel_getName(setterMethodName); 242 | 243 | const char* propertyName = setterName+3; 244 | const size_t propertyNameLength = strlen(propertyName)-1; // -1 to strip off the ':' from the end of the selector name 245 | 246 | char buffer[propertyNameLength+1]; // +1 for the terminating NUL 247 | memcpy(buffer, propertyName, propertyNameLength); 248 | buffer[propertyNameLength] = '\0'; 249 | 250 | // For a method name that looks like "setFoo:", the property could either be "Foo" or "foo". First, look up the capitalised version ("Foo"); if that doesn't exist, assume the property name is not capitalised ("foo"). 251 | if(class_getProperty(aClass, buffer) == NULL) buffer[0] = tolower(buffer[0]); 252 | 253 | return sel_getName(sel_registerName(buffer)); 254 | } 255 | 256 | template NSValue* NSValueMake(const T* pValue, const char* const typeEncoding) 257 | { 258 | return [NSValue valueWithBytes:pValue objCType:typeEncoding]; 259 | } 260 | 261 | #define DEFINE_NSVALUE_MAKE(typeName, uppercasedTypeName) \ 262 | template<> NSValue* NSValueMake(const typeName* pValue, const char* const) \ 263 | { \ 264 | return [NSNumber numberWith ## uppercasedTypeName:*pValue]; \ 265 | } 266 | 267 | DEFINE_NSVALUE_MAKE(bool, Bool); 268 | DEFINE_NSVALUE_MAKE(char, Char); 269 | DEFINE_NSVALUE_MAKE(double, Double); 270 | DEFINE_NSVALUE_MAKE(float, Float); 271 | DEFINE_NSVALUE_MAKE(int, Int); 272 | DEFINE_NSVALUE_MAKE(long, Long); 273 | DEFINE_NSVALUE_MAKE(long long, LongLong); 274 | DEFINE_NSVALUE_MAKE(short, Short); 275 | DEFINE_NSVALUE_MAKE(unsigned char, UnsignedChar); 276 | DEFINE_NSVALUE_MAKE(unsigned int, UnsignedInt); 277 | DEFINE_NSVALUE_MAKE(unsigned long, UnsignedLong); 278 | DEFINE_NSVALUE_MAKE(unsigned long long, UnsignedLongLong); 279 | DEFINE_NSVALUE_MAKE(unsigned short, UnsignedShort); 280 | 281 | template void SetRawValueFast(id const self, SEL _cmd, T value) 282 | { 283 | const char* const propertyName = PropertyNameFromSetterName([self class], _cmd); 284 | 285 | SetInstanceVariable(self, propertyName, &value); 286 | } 287 | 288 | template void SetRawValueSlow(id const self, SEL _cmd, T value) 289 | { 290 | const char* const propertyName = PropertyNameFromSetterName([self class], _cmd); 291 | 292 | MOLog(@"-[%@ %@]: %s (%s)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), propertyName, __PRETTY_FUNCTION__); 293 | 294 | Ivar const ivar = class_getInstanceVariable([self class], propertyName); 295 | T oldValue; 296 | GetInstanceVariable(self, ivar, &oldValue); 297 | 298 | if(value == oldValue) return; 299 | 300 | const char* const ivarTypeEncoding = ivar_getTypeEncoding(ivar); 301 | 302 | NSValue* valueObject = NSValueMake(&value, ivarTypeEncoding); 303 | NSValue* oldValueObject = NSValueMake(&value, ivarTypeEncoding); 304 | 305 | NSString* propertyNameObject = [NSString stringWithUTF8String:propertyName]; 306 | 307 | if([self respondsToSelector:@selector(propertyWillChange:from:to:)]) 308 | { 309 | const BOOL shouldChangeProperty = [(id)self propertyWillChange:propertyNameObject from:oldValueObject to:valueObject]; 310 | if(shouldChangeProperty) SetInstanceVariable(self, propertyName, &value); 311 | } 312 | else 313 | { 314 | SetInstanceVariable(self, propertyName, &value); 315 | } 316 | 317 | if([self respondsToSelector:@selector(propertyDidChange:from:to:)]) 318 | { 319 | [(id)self propertyDidChange:propertyNameObject from:oldValueObject to:valueObject]; 320 | } 321 | } 322 | 323 | template void SetRawIdValue(id const self, SEL _cmd, id value) 324 | { 325 | const char* const propertyName = PropertyNameFromSetterName([self class], _cmd); 326 | 327 | id oldValue; 328 | GetInstanceVariable(self, propertyName, &oldValue); 329 | 330 | if(value == oldValue) return; 331 | 332 | // Uninitialised intentionally 333 | NSString* propertyNameObject; 334 | switch(static_cast(slowMode)) 335 | { 336 | case true: 337 | { 338 | propertyNameObject = [NSString stringWithUTF8String:propertyName]; 339 | 340 | if([self respondsToSelector:@selector(propertyWillChange:from:to:)]) 341 | { 342 | const BOOL shouldChangeProperty = [(id)self propertyWillChange:propertyNameObject from:oldValue to:value]; 343 | if(!shouldChangeProperty) return; 344 | } 345 | 346 | break; 347 | } 348 | case false: 349 | break; 350 | } 351 | 352 | switch(assignmentMode) 353 | { 354 | case ObjCPropertyAssignmentModeAssign: 355 | break; 356 | case ObjCPropertyAssignmentModeRetain: 357 | if(!slowMode) [oldValue release]; 358 | value = [value retain]; 359 | break; 360 | case ObjCPropertyAssignmentModeCopy: 361 | if(!slowMode) [oldValue release]; 362 | value = [value copy]; 363 | break; 364 | } 365 | 366 | objc_assign_ivar(value, self, ivar_getOffset(class_getInstanceVariable([self class], propertyName))); 367 | 368 | switch(static_cast(slowMode)) 369 | { 370 | case true: 371 | if([self respondsToSelector:@selector(propertyDidChange:from:to:)]) [(id)self propertyDidChange:propertyNameObject from:oldValue to:value]; 372 | [oldValue release]; 373 | break; 374 | case false: 375 | break; 376 | } 377 | } 378 | 379 | static Method GetSetterMethod(Class const self, SEL const selector) 380 | { 381 | const char* const selectorName = sel_getName(selector); 382 | 383 | const size_t selectorNameLength = strlen(selectorName); 384 | char buffer[selectorNameLength+4+1]; // +4 for "Slow" or "Fast", +1 for terminating NUL 385 | memcpy(buffer, selectorName, selectorNameLength); 386 | 387 | if([self instancesRespondToSelector:@selector(propertyWillChange:from:to:)] 388 | || [self instancesRespondToSelector:@selector(propertyDidChange:from:to:)]) 389 | { 390 | strcpy(buffer+selectorNameLength-1, "Slow:"); 391 | } 392 | else 393 | { 394 | strcpy(buffer+selectorNameLength-1, "Fast:"); 395 | } 396 | 397 | return class_getInstanceMethod(self, sel_registerName(buffer)); 398 | } 399 | 400 | @interface RMModelObject () 401 | 402 | + (Method)_assignmentAccessorForTypeEncoding:(const char* const)typeEncoding wantsSetterMethod:(BOOL)wantsSetterMethod; 403 | 404 | @end 405 | 406 | @implementation RMModelObject 407 | 408 | + (void)load 409 | { 410 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bundleDidLoad:) name:NSBundleDidLoadNotification object:nil]; 411 | 412 | NSAutoreleasePool* pool = [NSAutoreleasePool new]; 413 | 414 | const int numberOfClasses = objc_getClassList(NULL, 0); 415 | 416 | std::vector classList(numberOfClasses); 417 | objc_getClassList(&classList[0], numberOfClasses); 418 | 419 | for(int i = 0; i < numberOfClasses; i++) 420 | { 421 | Class const theClass = classList[i]; 422 | if(class_getSuperclass(theClass) == self) RMModelObjectInitializeDynamicClass(theClass); 423 | } 424 | 425 | [pool drain]; 426 | } 427 | 428 | + (void)bundleDidLoad:(NSNotification*)note 429 | { 430 | NSArray* loadedClassNames = [[note userInfo] objectForKey:NSLoadedClasses]; 431 | if([loadedClassNames count] == 0) return; 432 | 433 | for(NSString* className in loadedClassNames) 434 | { 435 | Class theClass = objc_getClass([className UTF8String]); 436 | 437 | if(class_getSuperclass(theClass) == self) RMModelObjectInitializeDynamicClass(theClass); 438 | } 439 | } 440 | 441 | std::string StripDoubleQuotes(const char* const s) 442 | { 443 | std::string stripped; 444 | if(s == NULL) return stripped; 445 | 446 | bool insideDoubleQuotes = false; 447 | for(const char* p = s; *p != '\0'; p++) 448 | { 449 | if(*p == '"') insideDoubleQuotes = !insideDoubleQuotes; 450 | 451 | if(!insideDoubleQuotes && (*p != '"')) stripped.append(1, *p); 452 | } 453 | 454 | return stripped; 455 | } 456 | 457 | int RMTypeEncodingCompare(const char* lhs, const char* rhs) 458 | { 459 | const std::string& strippedLHS = StripDoubleQuotes(lhs); 460 | const std::string& strippedRHS = StripDoubleQuotes(rhs); 461 | 462 | return strippedLHS.compare(strippedRHS); 463 | } 464 | 465 | + (Method)_assignmentAccessorForTypeEncoding:(const char* const)typeEncoding wantsSetterMethod:(BOOL)wantsSetterMethod 466 | { 467 | switch(wantsSetterMethod) 468 | { 469 | case YES: 470 | switch(typeEncoding[0]) 471 | { 472 | case _C_ID: return GetSetterMethod(self, @selector(_modelObjectSetIdAssign:)); 473 | // _C_CLASS? 474 | // _C_SEL 475 | case _C_CHR: return GetSetterMethod(self, @selector(_modelObjectSetSignedChar:)); 476 | case _C_UCHR: return GetSetterMethod(self, @selector(_modelObjectSetUnsignedChar:)); 477 | case _C_SHT: return GetSetterMethod(self, @selector(_modelObjectSetSignedShort:)); 478 | case _C_USHT: return GetSetterMethod(self, @selector(_modelObjectSetUnsignedShort:)); 479 | case _C_INT: return GetSetterMethod(self, @selector(_modelObjectSetSignedInt:)); 480 | case _C_UINT: return GetSetterMethod(self, @selector(_modelObjectSetUnsignedInt:)); 481 | case _C_LNG: return GetSetterMethod(self, @selector(_modelObjectSetSignedLong:)); 482 | case _C_ULNG: return GetSetterMethod(self, @selector(_modelObjectSetUnsignedLong:)); 483 | case _C_LNG_LNG: return GetSetterMethod(self, @selector(_modelObjectSetSignedLongLong:)); 484 | case _C_ULNG_LNG: return GetSetterMethod(self, @selector(_modelObjectSetUnsignedLongLong:)); 485 | case _C_FLT: return GetSetterMethod(self, @selector(_modelObjectSetFloat:)); 486 | case _C_DBL: return GetSetterMethod(self, @selector(_modelObjectSetDouble:)); 487 | // _C_BFLD 488 | // _C_BOOL 489 | // _C_VOID? 490 | // _C_UNDEF 491 | case _C_PTR: return GetSetterMethod(self, @selector(_modelObjectSetPointer:)); 492 | case _C_CHARPTR: return GetSetterMethod(self, @selector(_modelObjectSetCharPointer:)); 493 | // _C_CHARPTR 494 | // _C_ATOM 495 | // _C_ARY_B 496 | // _C_ARY_E 497 | // _C_UNION_B 498 | // _C_UNION_E 499 | case _C_STRUCT_B: 500 | if(RMTypeEncodingCompare(typeEncoding, @encode(CGRect)) == 0) return GetSetterMethod(self, @selector(_modelObjectSetCGRect:)); 501 | else if(RMTypeEncodingCompare(typeEncoding, @encode(CGSize)) == 0) return GetSetterMethod(self, @selector(_modelObjectSetCGSize:)); 502 | else if(RMTypeEncodingCompare(typeEncoding, @encode(CGPoint)) == 0) return GetSetterMethod(self, @selector(_modelObjectSetCGPoint:)); 503 | else if(RMTypeEncodingCompare(typeEncoding, @encode(NSRange)) == 0) return GetSetterMethod(self, @selector(_modelObjectSetNSRange:)); 504 | // _C_STRUCT_E 505 | // _C_VECTOR 506 | // _C_CONST 507 | default: 508 | NSAssert1(NO, @"Unknown type encoding: %s", typeEncoding); 509 | } 510 | default: 511 | case NO: 512 | switch(typeEncoding[0]) 513 | { 514 | case _C_ID: return class_getInstanceMethod(self, @selector(_modelObjectGetIdAssign)); 515 | // _C_CLASS 516 | // _C_SEL 517 | case _C_CHR: return class_getInstanceMethod(self, @selector(_modelObjectGetSignedChar)); 518 | case _C_UCHR: return class_getInstanceMethod(self, @selector(_modelObjectGetUnsignedChar)); 519 | case _C_SHT: return class_getInstanceMethod(self, @selector(_modelObjectGetSignedShort)); 520 | case _C_USHT: return class_getInstanceMethod(self, @selector(_modelObjectGetUnsignedShort)); 521 | case _C_INT: return class_getInstanceMethod(self, @selector(_modelObjectGetSignedInt)); 522 | case _C_UINT: return class_getInstanceMethod(self, @selector(_modelObjectGetUnsignedInt)); 523 | case _C_LNG: return class_getInstanceMethod(self, @selector(_modelObjectGetSignedLong)); 524 | case _C_ULNG: return class_getInstanceMethod(self, @selector(_modelObjectGetUnsignedLong)); 525 | case _C_LNG_LNG: return class_getInstanceMethod(self, @selector(_modelObjectGetSignedLongLong)); 526 | case _C_ULNG_LNG: return class_getInstanceMethod(self, @selector(_modelObjectGetUnsignedLongLong)); 527 | case _C_FLT: return class_getInstanceMethod(self, @selector(_modelObjectGetFloat)); 528 | case _C_DBL: return class_getInstanceMethod(self, @selector(_modelObjectGetDouble)); 529 | // _C_BFLD 530 | // _C_BOOL 531 | // _C_VOID? 532 | // _C_UNDEF 533 | case _C_PTR: return class_getInstanceMethod(self, @selector(_modelObjectGetPointer)); 534 | case _C_CHARPTR: return class_getInstanceMethod(self, @selector(_modelObjectGetCharPointer)); 535 | // _C_ATOM 536 | // _C_ARY_B 537 | // _C_ARY_E 538 | // _C_UNION_B 539 | // _C_UNION_E 540 | case _C_STRUCT_B: 541 | if(RMTypeEncodingCompare(typeEncoding, @encode(CGRect)) == 0) return class_getInstanceMethod(self, @selector(_modelObjectGetCGRect)); 542 | else if(RMTypeEncodingCompare(typeEncoding, @encode(CGSize)) == 0) return class_getInstanceMethod(self, @selector(_modelObjectGetCGSize)); 543 | else if(RMTypeEncodingCompare(typeEncoding, @encode(CGPoint)) == 0) return class_getInstanceMethod(self, @selector(_modelObjectGetCGPoint)); 544 | else if(RMTypeEncodingCompare(typeEncoding, @encode(NSRange)) == 0) return class_getInstanceMethod(self, @selector(_modelObjectGetNSRange)); 545 | // _C_STRUCT_E 546 | // _C_VECTOR 547 | // _C_CONST 548 | default: 549 | NSAssert1(NO, @"Unknown type encoding: %s", typeEncoding); 550 | } 551 | } 552 | 553 | return NULL; 554 | } 555 | 556 | #define DEFINE_PRIMITIVE_GETTER_METHOD(typeName, uppercasedTypeName) \ 557 | - (typeName)_modelObjectGet ## uppercasedTypeName \ 558 | { \ 559 | return GetRawValue(self, _cmd); \ 560 | } 561 | 562 | #define DEFINE_PRIMITIVE_SETTER_METHOD_SLOW(typeName, uppercasedTypeName) \ 563 | - (void)_modelObjectSet ## uppercasedTypeName ## Slow:(typeName)value \ 564 | { \ 565 | SetRawValueSlow(self, _cmd, value); \ 566 | } 567 | 568 | #define DEFINE_PRIMITIVE_SETTER_METHOD_FAST(typeName, uppercasedTypeName) \ 569 | - (void)_modelObjectSet ## uppercasedTypeName ## Fast:(typeName)value \ 570 | { \ 571 | SetRawValueFast(self, _cmd, value); \ 572 | } 573 | 574 | #define DEFINE_PRIMITIVE_ACCESSOR_METHODS(typeName, uppercasedTypeName) \ 575 | DEFINE_PRIMITIVE_GETTER_METHOD(typeName, uppercasedTypeName) \ 576 | DEFINE_PRIMITIVE_SETTER_METHOD_SLOW(typeName, uppercasedTypeName) \ 577 | DEFINE_PRIMITIVE_SETTER_METHOD_FAST(typeName, uppercasedTypeName) 578 | 579 | static inline bool operator==(const CGPoint lhs, const CGPoint rhs) 580 | { 581 | return CGPointEqualToPoint(lhs, rhs); 582 | } 583 | 584 | static inline bool operator==(const CGRect lhs, const CGRect rhs) 585 | { 586 | return CGRectEqualToRect(lhs, rhs); 587 | } 588 | 589 | static inline bool operator==(const CGSize lhs, const CGSize rhs) 590 | { 591 | return CGSizeEqualToSize(lhs, rhs); 592 | } 593 | 594 | static inline bool operator==(const NSRange lhs, const NSRange rhs) 595 | { 596 | return NSEqualRanges(lhs, rhs); 597 | } 598 | 599 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(signed char, SignedChar); 600 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(signed int, SignedInt); 601 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(signed short, SignedShort); 602 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(signed long, SignedLong); 603 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(signed long long, SignedLongLong); 604 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(unsigned char, UnsignedChar); 605 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(unsigned int, UnsignedInt); 606 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(unsigned short, UnsignedShort); 607 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(unsigned long, UnsignedLong); 608 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(unsigned long long, UnsignedLongLong); 609 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(float, Float); 610 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(double, Double); 611 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(void*, Pointer); 612 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(char*, CharPointer); 613 | 614 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(CGPoint, CGPoint); 615 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(CGRect, CGRect); 616 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(CGSize, CGSize); 617 | DEFINE_PRIMITIVE_ACCESSOR_METHODS(NSRange, NSRange); 618 | 619 | - (id)_modelObjectGetIdAssign 620 | { 621 | return GetRawValue(self, _cmd); 622 | } 623 | 624 | - (id)_modelObjectGetIdRetain 625 | { 626 | return [[GetRawValue(self, _cmd) retain] autorelease]; 627 | } 628 | 629 | - (id)_modelObjectGetIdCopy 630 | { 631 | return [[GetRawValue(self, _cmd) copy] autorelease]; 632 | } 633 | 634 | - (void)_modelObjectSetIdAssignSlow:(id)value 635 | { 636 | SetRawIdValue(self, _cmd, value); 637 | } 638 | 639 | - (void)_modelObjectSetIdRetainSlow:(id)value 640 | { 641 | SetRawIdValue(self, _cmd, value); 642 | } 643 | 644 | - (void)_modelObjectSetIdCopySlow:(id)value 645 | { 646 | SetRawIdValue(self, _cmd, value); 647 | } 648 | 649 | - (void)_modelObjectSetIdAssignFast:(id)value 650 | { 651 | SetRawIdValue(self, _cmd, value); 652 | } 653 | 654 | - (void)_modelObjectSetIdRetainFast:(id)value 655 | { 656 | SetRawIdValue(self, _cmd, value); 657 | } 658 | 659 | - (void)_modelObjectSetIdCopyFast:(id)value 660 | { 661 | SetRawIdValue(self, _cmd, value); 662 | } 663 | 664 | static inline id RMAllocateObject(Class self, NSZone* zone) 665 | { 666 | id allocatedObject = NSAllocateObject(self, 0, zone); 667 | if(allocatedObject == nil) 668 | { 669 | NSLog(@"NSAllocateObject(%@[%p], 0, %@) returned nil", NSStringFromClass([self class]), self, zone); 670 | 671 | return nil; 672 | } 673 | 674 | return allocatedObject; 675 | } 676 | 677 | + (id)allocWithZone:(NSZone*)zone 678 | { 679 | Class dynamicClass = RMModelObjectInitializeDynamicClass(self); 680 | 681 | return RMAllocateObject(dynamicClass, zone); 682 | } 683 | 684 | static BOOL inline RMClassAddMethod(Class cls, SEL name, IMP imp, const char* types) 685 | { 686 | if(imp == NULL) 687 | { 688 | NSLog(@"RMClassAddMethod() passed a NULL IMP for %@", NSStringFromSelector(name)); 689 | return NO; 690 | } 691 | 692 | const BOOL didAddMethod = class_addMethod(cls, name, imp, types); 693 | if(!didAddMethod) NSLog(@"class_addMethod returned NO for `%@' (IMP=%p, typeEncoding=%s)", NSStringFromSelector(name), imp, types); 694 | 695 | return didAddMethod; 696 | } 697 | 698 | Class RMModelObjectInitializeDynamicClass(Class self) 699 | { 700 | NSString* className = NSStringFromClass([self class]); 701 | 702 | if([className hasPrefix:@"RMModelObject_"]) return objc_getClass([className UTF8String]); 703 | 704 | NSString* dynamicClassName = [NSString stringWithFormat:@"RMModelObject_%@", className]; 705 | if(Class existingDynamicClass = objc_getClass([dynamicClassName UTF8String])) return existingDynamicClass; 706 | 707 | Class dynamicClass = objc_allocateClassPair(self, [dynamicClassName UTF8String], 0); 708 | if(dynamicClass == Nil) 709 | { 710 | NSLog(@"objc_allocateClassPair returned NULL"); 711 | return nil; 712 | } 713 | 714 | unsigned numberOfProperties = 0; 715 | objc_property_t* properties = class_copyPropertyList(self, &numberOfProperties); 716 | 717 | if (numberOfProperties == 0) 718 | @throw [NSException exceptionWithName:@"InvalidClassException" reason: 719 | [NSString stringWithFormat:@"You must define at least one dynamic property on the RMModelObject class \"%@\" (or it will just crash anyway)", className] userInfo:nil]; 720 | 721 | MOLog(@"RMModelObjectInitializeDynamicClass for self=%@ found %u properties", self, numberOfProperties); 722 | 723 | for(objc_property_t* property = properties; 724 | property < properties+numberOfProperties; 725 | property++) 726 | { 727 | const char* const propertyName = property_getName(*property); 728 | ObjCPropertyAttributes propertyAttributes(property_getAttributes(*property)); 729 | 730 | if(propertyAttributes.isReadOnly) continue; 731 | if(!propertyAttributes.isDynamic) continue; 732 | if(propertyAttributes.isSynthesized) continue; 733 | 734 | if(propertyAttributes.isWeak) 735 | { 736 | NSCAssert3(NO, @"%s: __weak qualifier not yet supported for class named %@, property named %s, terminating immediately to prevent further unexpected behaviour", __func__, className, propertyName); 737 | } 738 | 739 | const char* const propertyTypeEncoding = propertyAttributes.typeEncoding.c_str(); 740 | if(propertyTypeEncoding == NULL) 741 | { 742 | NSCAssert2(NO, @"%s: property type encoding is NULL for property name %s, terminating immediately to prevent further unexpected behaviour", __func__, propertyName); 743 | } 744 | 745 | NSUInteger propertySize = 0; 746 | NSUInteger propertyAlignment = 0; 747 | NSGetSizeAndAlignment(propertyTypeEncoding, &propertySize, &propertyAlignment); 748 | 749 | MOLog(@"%s has size=%lu (alignment=%lu, %lu)", propertyTypeEncoding, propertySize, propertyAlignment, (NSUInteger)log2(propertyAlignment)); 750 | 751 | const BOOL didAddIvar = class_addIvar(dynamicClass, propertyName, propertySize, log2(propertyAlignment), propertyTypeEncoding); 752 | if(!didAddIvar) 753 | { 754 | NSLog(@"class_addIvar failed for name=%s, typeEncoding=%s, size=%lu, alignment=%lu", propertyName, propertyTypeEncoding, propertySize, propertyAlignment); 755 | free(properties); 756 | return nil; 757 | } 758 | 759 | const char* const getterName = propertyName; 760 | 761 | size_t bufferSize = strlen(propertyName)+3+1+1; // +3 for "set", +1 for ":", +1 for NUL 762 | char buffer[bufferSize]; 763 | const int charactersPrinted = snprintf(buffer, bufferSize, "set%c%s:", islower(propertyName[0]) ? toupper(propertyName[0]) : propertyName[0], propertyName+1); 764 | if(charactersPrinted != (int)bufferSize-1) 765 | { 766 | NSLog(@"snprintf() of %c%s: wrote %d characters, which is different from the buffer size of %zu-1", 767 | islower(propertyName[0]) ? toupper(propertyName[0]) : propertyName[0], propertyName+1, charactersPrinted, bufferSize); 768 | free(properties); 769 | return nil; 770 | } 771 | 772 | const char* const setterName = buffer; 773 | 774 | Method getter = NULL; 775 | if(class_getInstanceMethod(self, sel_registerName(getterName)) == NULL) 776 | { 777 | if(propertyAttributes.assignmentMode == ObjCPropertyAssignmentModeAssign) getter = (Method)[self _assignmentAccessorForTypeEncoding:propertyTypeEncoding wantsSetterMethod:NO]; 778 | else if(propertyAttributes.assignmentMode == ObjCPropertyAssignmentModeRetain) getter = class_getInstanceMethod(self, @selector(_modelObjectGetIdRetain)); 779 | else if(propertyAttributes.assignmentMode == ObjCPropertyAssignmentModeCopy) getter = class_getInstanceMethod(self, @selector(_modelObjectGetIdCopy)); 780 | 781 | RMClassAddMethod(dynamicClass, sel_registerName(getterName), method_getImplementation(getter), method_getTypeEncoding(getter)); 782 | } 783 | 784 | Method setter = NULL; 785 | if(propertyAttributes.assignmentMode == ObjCPropertyAssignmentModeAssign) setter = (Method)[self _assignmentAccessorForTypeEncoding:propertyTypeEncoding wantsSetterMethod:YES]; 786 | else if(propertyAttributes.assignmentMode == ObjCPropertyAssignmentModeRetain) setter = GetSetterMethod(self, @selector(_modelObjectSetIdRetain:)); 787 | else if(propertyAttributes.assignmentMode == ObjCPropertyAssignmentModeCopy) setter = GetSetterMethod(self, @selector(_modelObjectSetIdCopy:)); 788 | 789 | RMClassAddMethod(dynamicClass, sel_registerName(setterName), method_getImplementation(setter), method_getTypeEncoding(setter)); 790 | 791 | class_addProtocol(dynamicClass, @protocol(NSCoding)); 792 | class_addProtocol(dynamicClass, @protocol(NSCopying)); 793 | } 794 | 795 | objc_registerClassPair(dynamicClass); 796 | 797 | free(properties); 798 | return dynamicClass; 799 | } 800 | 801 | - (void)dealloc 802 | { 803 | /*unsigned int numberOfIvars = 0; 804 | Ivar* ivars = class_copyIvarList([self class], &numberOfIvars); 805 | 806 | for(const Ivar* p = ivars; p < ivars+numberOfIvars; p++) 807 | { 808 | Ivar const ivar = *p; 809 | }*/ 810 | 811 | FOR_ALL_IVARS(ivar, self) 812 | { 813 | MOLog(@"%@: deallocating %s...", NSStringFromClass([self class]), ivar_getName(ivar)); 814 | if(ivar_getTypeEncoding(ivar)[0] == _C_ID) [self setValue:nil forKey:[NSString stringWithUTF8String:ivar_getName(ivar)]]; 815 | } 816 | 817 | FREE_FOR_ALL; 818 | [super dealloc]; 819 | } 820 | 821 | #pragma mark NSCopying 822 | 823 | static id CopyObjectInto(id self, id copiedObject, NSZone* zone, const BOOL mutableCopy) 824 | { 825 | if (!copiedObject) 826 | copiedObject = [[[self class] alloc] init]; 827 | 828 | MOLog(@"copied object size=%lu, self size=%lu", class_getInstanceSize([copiedObject class]), class_getInstanceSize([self class])); 829 | 830 | FOR_ALL_IVARS(ivar, self) 831 | { 832 | const char* ivarName = ivar_getName(ivar); 833 | 834 | objc_property_t property = class_getProperty([self class], ivarName); 835 | if(property == NULL) continue; 836 | 837 | const ptrdiff_t ivarOffset = ivar_getOffset(ivar); 838 | 839 | MOLog(@"about to copy %s (offset=%ld)...", ivarName, ivarOffset); 840 | 841 | // We assume that the property to copy is of type id: if it isn't, it doesn't matter anyway since we figure out the real size of the object using NSGetSizeAndAlignment() and do an approprate memcpy. If it's a proper id, it simplifies the code a bit since we don't need to cast void*'s to ids. 842 | id* destinationIvarLocation = reinterpret_cast( (char*)copiedObject+ivarOffset ); 843 | id* sourceIvarLocation = reinterpret_cast( (char*)self+ivarOffset ); 844 | 845 | ObjCPropertyAttributes propertyAttributes(property_getAttributes(property)); 846 | switch(propertyAttributes.assignmentMode) 847 | { 848 | case ObjCPropertyAssignmentModeAssign: 849 | { 850 | NSUInteger ivarSize = 0; 851 | NSUInteger ivarAlignment = 0; 852 | NSGetSizeAndAlignment(ivar_getTypeEncoding(ivar), &ivarSize, &ivarAlignment); 853 | 854 | MOLog(@"pre-memcpy() from %p (%p+%ld) to %p, size=%lu", sourceIvarLocation, self, ivarOffset, destinationIvarLocation, ivarSize); 855 | 856 | memcpy(destinationIvarLocation, sourceIvarLocation, ivarSize); 857 | 858 | MOLog(@"post-memcpy()"); 859 | break; 860 | } 861 | case ObjCPropertyAssignmentModeRetain: 862 | *destinationIvarLocation = [*sourceIvarLocation retain]; 863 | break; 864 | case ObjCPropertyAssignmentModeCopy: 865 | if(mutableCopy && [*sourceIvarLocation respondsToSelector:@selector(mutableCopyWithZone:)]) *destinationIvarLocation = [*sourceIvarLocation mutableCopy]; 866 | else *destinationIvarLocation = [*sourceIvarLocation copy]; 867 | break; 868 | } 869 | 870 | MOLog(@"ivar %s copied", ivarName); 871 | } 872 | 873 | MOLog(@"About to return copied object"); 874 | 875 | // TODO: call [super copy] 876 | 877 | FREE_FOR_ALL; 878 | return copiedObject; 879 | } 880 | 881 | - (id)copyInto:(id)receiver withZone:(NSZone*)zone 882 | { 883 | return CopyObjectInto(self, receiver, zone, NO); 884 | } 885 | 886 | - (id)copyWithZone:(NSZone*)zone 887 | { 888 | return CopyObjectInto(self, nil, zone, NO); 889 | } 890 | 891 | - (id)mutableCopyWithZone:(NSZone*)zone 892 | { 893 | return CopyObjectInto(self, nil, zone, YES); 894 | } 895 | 896 | #pragma mark NSCoding 897 | 898 | - (id)initWithCoder:(NSCoder*)decoder 899 | { 900 | MOLog(@"initWithCoder 1"); 901 | 902 | self = [super init]; 903 | if(self == nil) return nil; 904 | 905 | MOLog(@"initWithCoder 2"); 906 | 907 | unsigned int numberOfIvars = 0; 908 | Ivar* ivars = class_copyIvarList([self class], &numberOfIvars); 909 | 910 | if([decoder allowsKeyedCoding]) 911 | { 912 | for(const Ivar* p = ivars; p < ivars+numberOfIvars; p++) 913 | { 914 | Ivar const ivar = *p; 915 | 916 | NSString* ivarNameString = [NSString stringWithUTF8String:ivar_getName(ivar)]; 917 | 918 | if([decoder containsValueForKey:ivarNameString]) 919 | { 920 | id object = [decoder decodeObjectForKey:ivarNameString]; 921 | [self setValue:object forKey:ivarNameString]; 922 | 923 | MOLog(@"Setting object %p (%@) for key %@", object, object, ivarNameString); 924 | } 925 | } 926 | } 927 | else 928 | { 929 | id object = [decoder decodeObject]; 930 | 931 | if(object == nil) 932 | { 933 | MOLog(@"-[%@ initWithCoder:]: unkeyed decoder doesn't have an initial object", NSStringFromClass([self class])); 934 | 935 | [self release]; 936 | free(ivars); 937 | return nil; 938 | } 939 | 940 | NSDictionary* dictionary = [object isKindOfClass:[NSDictionary class]] ? (NSDictionary*)object : nil; 941 | if(dictionary == nil) 942 | { 943 | MOLog(@"-[%@ initWithCoder:]: unkeyed decoder's initial object is not an NSDictionary", NSStringFromClass([self class])); 944 | 945 | [self release]; 946 | free(ivars); 947 | return nil; 948 | } 949 | 950 | for(const Ivar* p = ivars; p < ivars+numberOfIvars; p++) 951 | { 952 | Ivar const ivar = *p; 953 | 954 | NSString* ivarNameString = [NSString stringWithUTF8String:ivar_getName(ivar)]; 955 | 956 | if([dictionary objectForKey:ivarNameString]) 957 | { 958 | id object = [dictionary objectForKey:ivarNameString]; 959 | [self setValue:object forKey:ivarNameString]; 960 | } 961 | } 962 | } 963 | 964 | free(ivars); 965 | return self; 966 | } 967 | 968 | - (void)encodeWithCoder:(NSCoder*)encoder 969 | { 970 | // This is only used if the encoder doesn't support keyed coding 971 | NSMutableDictionary* encodingDictionary = [NSMutableDictionary dictionary]; 972 | 973 | FOR_ALL_IVARS(ivar, self) 974 | { 975 | NSString* ivarNameString = [NSString stringWithUTF8String:ivar_getName(ivar)]; 976 | 977 | id object = [self valueForKey:ivarNameString]; 978 | 979 | MOLog(@"encoding property name %@: %@ (%@)", ivarNameString, object, NSStringFromClass([object class])); 980 | 981 | if(object) 982 | { 983 | if([encoder allowsKeyedCoding]) [encoder encodeObject:object forKey:ivarNameString]; 984 | else [encodingDictionary setObject:object forKey:ivarNameString]; 985 | } 986 | } 987 | 988 | if(![encoder allowsKeyedCoding]) [encoder encodeObject:encodingDictionary]; 989 | FREE_FOR_ALL; 990 | } 991 | 992 | #pragma mark NSObject 993 | 994 | - (NSString*)description 995 | { 996 | NSMutableString* const description = [NSMutableString string]; 997 | 998 | [description appendFormat:@"<%@ %p: ", NSStringFromClass([[self superclass] class]), self]; 999 | 1000 | NSString* separator = @"\n "; 1001 | 1002 | FOR_ALL_IVARS(ivar, self) 1003 | { 1004 | const char* const ivarName = ivar_getName(ivar); 1005 | 1006 | objc_property_t const property = class_getProperty([self class], ivarName); 1007 | if(property == NULL) continue; 1008 | 1009 | const ptrdiff_t ivarOffset = ivar_getOffset(ivar); 1010 | 1011 | MOLog(@"about to describe %s (offset=%ld)...", ivarName, ivarOffset); 1012 | 1013 | // We assume that the property to inspect is of type id: if it isn't, it doesn't matter anyway since we figure out the real size of the object using NSGetSizeAndAlignment() and do an approprate memcmp. If it's a proper id, it simplifies the code a bit since we don't need to cast void*'s to ids. 1014 | id* ivarLocation = reinterpret_cast( (char*)self+ivarOffset ); 1015 | 1016 | const char* const ivarTypeEncoding = ivar_getTypeEncoding(ivar); 1017 | NSString* format = nil; 1018 | switch(ivarTypeEncoding[0]) 1019 | { 1020 | case _C_ID: 1021 | { 1022 | id const object = (id)*ivarLocation; 1023 | if ([object respondsToSelector:@selector(descriptionWithLocale:indent:)]) 1024 | [description appendFormat:@"%@%s = %@", separator, ivarName, [object descriptionWithLocale:nil indent:1]]; 1025 | else 1026 | [description appendFormat:@"%@%s = %@", separator, ivarName, object]; 1027 | break; 1028 | } 1029 | case _C_INT: format = @"%i"; break; 1030 | case _C_UINT: format = @"%u"; break; 1031 | case _C_SHT: format = @"%hi"; break; 1032 | case _C_USHT: format = @"%hu"; break; 1033 | case _C_LNG: format = @"%li"; break; 1034 | case _C_ULNG: format = @"%lu"; break; 1035 | case _C_CHR: format = @"%hhi"; break; 1036 | case _C_UCHR: format = @"%hhu"; break; 1037 | case _C_LNG_LNG: format = @"%lli"; break; 1038 | case _C_ULNG_LNG: format = @"%llu"; break; 1039 | case _C_FLT: format = @"%f"; break; 1040 | case _C_DBL: format = @"%f"; break; 1041 | case _C_STRUCT_B: 1042 | if(RMTypeEncodingCompare(ivarTypeEncoding, @encode(CGRect)) == 0) 1043 | { 1044 | [description appendFormat:@"%@%s = (rect) %@", separator, ivarName, NSStringFromCGRect(*(CGRect*)ivarLocation)]; 1045 | break; 1046 | } 1047 | else if(RMTypeEncodingCompare(ivarTypeEncoding, @encode(CGSize)) == 0) 1048 | { 1049 | [description appendFormat:@"%@%s = (size) %@", separator, ivarName, NSStringFromCGSize(*(CGSize*)ivarLocation)]; 1050 | break; 1051 | } 1052 | else if(RMTypeEncodingCompare(ivarTypeEncoding, @encode(CGPoint)) == 0) 1053 | { 1054 | [description appendFormat:@"%@%s = (point) %@", separator, ivarName, NSStringFromCGPoint(*(CGPoint*)ivarLocation)]; 1055 | break; 1056 | } 1057 | else if(RMTypeEncodingCompare(ivarTypeEncoding, @encode(NSRange)) == 0) 1058 | { 1059 | [description appendFormat:@"%@%s = (range) %@", separator, ivarName, NSStringFromRange(*(NSRange*)ivarLocation)]; 1060 | break; 1061 | } 1062 | default: 1063 | [description appendFormat:@"%@%s = ?", separator, ivarName]; 1064 | break; 1065 | } 1066 | 1067 | if(format != nil) 1068 | { 1069 | [description appendFormat:@"%@%s = ", separator, ivarName]; 1070 | 1071 | if (ivarTypeEncoding[0] == _C_FLT) 1072 | [description appendFormat:format, *((float*)ivarLocation)]; // else you get 0.0000000 1073 | else if (ivarTypeEncoding[0] == _C_DBL) 1074 | [description appendFormat:format, *((double*)ivarLocation)]; // else you get 0.0000000 1075 | else 1076 | [description appendFormat:format, *ivarLocation]; 1077 | } 1078 | separator = @"\n "; 1079 | } 1080 | 1081 | [description appendFormat:@">"]; 1082 | 1083 | FREE_FOR_ALL; 1084 | return description; 1085 | } 1086 | 1087 | - (BOOL)isEqual:(id)other 1088 | { 1089 | if([other isKindOfClass:[self class]]) return [self isEqualToModelObject:other]; 1090 | else return NO; 1091 | } 1092 | 1093 | - (BOOL)isEqualToModelObject:(RMModelObject*)other 1094 | { 1095 | FOR_ALL_IVARS(ivar, self) 1096 | { 1097 | const char* ivarName = ivar_getName(ivar); 1098 | 1099 | objc_property_t property = class_getProperty([self class], ivarName); 1100 | if(property == NULL) continue; 1101 | 1102 | const ptrdiff_t ivarOffset = ivar_getOffset(ivar); 1103 | 1104 | MOLog(@"about to check equality of %s (offset=%ld)...", ivarName, ivarOffset); 1105 | 1106 | // We assume that the property to inspect is of type id: if it isn't, it doesn't matter anyway since we figure out the real size of the object using NSGetSizeAndAlignment() and do an approprate memcmp. If it's a proper id, it simplifies the code a bit since we don't need to cast void*'s to ids. 1107 | id* sourceIvarLocation = reinterpret_cast( (char*)self+ivarOffset ); 1108 | id* otherIvarLocation = reinterpret_cast( (char*)other+ivarOffset ); 1109 | 1110 | const char* const ivarTypeEncoding = ivar_getTypeEncoding(ivar); 1111 | switch(ivarTypeEncoding[0]) 1112 | { 1113 | case _C_ID: 1114 | { 1115 | if(*sourceIvarLocation == nil && *otherIvarLocation == nil) continue; 1116 | 1117 | else if(![*sourceIvarLocation isEqual:*otherIvarLocation]) 1118 | { 1119 | MOLog(@"isEqual: for %s (type id) returned NO: src=%@, other=%@", 1120 | ivarName, *sourceIvarLocation, *otherIvarLocation); 1121 | FREE_FOR_ALL; 1122 | return NO; 1123 | } 1124 | break; 1125 | } 1126 | default: 1127 | { 1128 | NSUInteger ivarSize = 0; 1129 | NSUInteger ivarAlignment = 0; 1130 | NSGetSizeAndAlignment(ivar_getTypeEncoding(ivar), &ivarSize, &ivarAlignment); 1131 | 1132 | MOLog(@"pre-memcmp() of %p (%p+%ld) with %p, size=%lu", 1133 | sourceIvarLocation, self, ivarOffset, otherIvarLocation, ivarSize); 1134 | 1135 | const int memcmpResult = memcmp(sourceIvarLocation, otherIvarLocation, ivarSize); 1136 | if(memcmpResult != 0) 1137 | { 1138 | MOLog(@"isEqual: for %s (type %s) returned NO", ivarName, ivarTypeEncoding); 1139 | FREE_FOR_ALL; 1140 | return NO; 1141 | } 1142 | 1143 | MOLog(@"post-memcmp()"); 1144 | break; 1145 | } 1146 | } 1147 | 1148 | MOLog(@"ivar %s passed equality check", ivarName); 1149 | } 1150 | 1151 | FREE_FOR_ALL; 1152 | return YES; 1153 | } 1154 | 1155 | - (NSUInteger)hash 1156 | { 1157 | // This is an incredibly braindead hash function, but see . I quote: "If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the hash method of the object must not change while the object is in the collection. Therefore, either the hash method must not rely on any of the object’s internal state information or you must make sure the object’s internal state information does not change while the object is in the collection." While this sounds completely braindead to me, I'm erring on the safe side rather than risking Cocoa non-compliance. If you have any hints for this, please let me know. 1158 | 1159 | NSUInteger hash = 0; 1160 | 1161 | FOR_ALL_IVARS(ivar, self) hash += (NSUInteger)ivar; 1162 | 1163 | FREE_FOR_ALL; 1164 | return hash; 1165 | } 1166 | 1167 | @end 1168 | 1169 | //*************************************************************************** 1170 | -------------------------------------------------------------------------------- /RMModelObject.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 42; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7B64C0370E61438700E4D136 /* RMModelObjectTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B64C01D0E61430100E4D136 /* RMModelObjectTest.m */; }; 11 | 7B64C0390E61438900E4D136 /* RMModelObject.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7B64C01B0E61429C00E4D136 /* RMModelObject.mm */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 7B64C01A0E61429C00E4D136 /* RMModelObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMModelObject.h; sourceTree = ""; }; 16 | 7B64C01B0E61429C00E4D136 /* RMModelObject.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RMModelObject.mm; sourceTree = ""; }; 17 | 7B64C01D0E61430100E4D136 /* RMModelObjectTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMModelObjectTest.m; sourceTree = ""; }; 18 | 7B64C0300E61438200E4D136 /* RMModelObjectTest.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RMModelObjectTest.octest; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 7B64C0320E61438200E4D136 /* RMModelObjectTest-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RMModelObjectTest-Info.plist"; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXFrameworksBuildPhase section */ 23 | 7B64C02D0E61438200E4D136 /* Frameworks */ = { 24 | isa = PBXFrameworksBuildPhase; 25 | buildActionMask = 2147483647; 26 | files = ( 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXFrameworksBuildPhase section */ 31 | 32 | /* Begin PBXGroup section */ 33 | 7B64C00D0E61428C00E4D136 = { 34 | isa = PBXGroup; 35 | children = ( 36 | 7B64C01A0E61429C00E4D136 /* RMModelObject.h */, 37 | 7B64C01D0E61430100E4D136 /* RMModelObjectTest.m */, 38 | 7B64C01B0E61429C00E4D136 /* RMModelObject.mm */, 39 | 7B64C0310E61438200E4D136 /* Products */, 40 | 7B64C0320E61438200E4D136 /* RMModelObjectTest-Info.plist */, 41 | ); 42 | sourceTree = ""; 43 | }; 44 | 7B64C0310E61438200E4D136 /* Products */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 7B64C0300E61438200E4D136 /* RMModelObjectTest.octest */, 48 | ); 49 | name = Products; 50 | sourceTree = ""; 51 | }; 52 | /* End PBXGroup section */ 53 | 54 | /* Begin PBXNativeTarget section */ 55 | 7B64C02F0E61438200E4D136 /* RMModelObjectTest */ = { 56 | isa = PBXNativeTarget; 57 | buildConfigurationList = 7B64C0350E61438200E4D136 /* Build configuration list for PBXNativeTarget "RMModelObjectTest" */; 58 | buildPhases = ( 59 | 7B64C02B0E61438200E4D136 /* Resources */, 60 | 7B64C02C0E61438200E4D136 /* Sources */, 61 | 7B64C02D0E61438200E4D136 /* Frameworks */, 62 | 7B64C02E0E61438200E4D136 /* ShellScript */, 63 | ); 64 | buildRules = ( 65 | ); 66 | dependencies = ( 67 | ); 68 | name = RMModelObjectTest; 69 | productName = RMModelObjectTest; 70 | productReference = 7B64C0300E61438200E4D136 /* RMModelObjectTest.octest */; 71 | productType = "com.apple.product-type.bundle"; 72 | }; 73 | /* End PBXNativeTarget section */ 74 | 75 | /* Begin PBXProject section */ 76 | 7B64C00F0E61428C00E4D136 /* Project object */ = { 77 | isa = PBXProject; 78 | buildConfigurationList = 7B64C0120E61428C00E4D136 /* Build configuration list for PBXProject "RMModelObject" */; 79 | compatibilityVersion = "Xcode 2.4"; 80 | hasScannedForEncodings = 0; 81 | mainGroup = 7B64C00D0E61428C00E4D136; 82 | productRefGroup = 7B64C0310E61438200E4D136 /* Products */; 83 | projectDirPath = ""; 84 | projectRoot = ""; 85 | targets = ( 86 | 7B64C02F0E61438200E4D136 /* RMModelObjectTest */, 87 | ); 88 | }; 89 | /* End PBXProject section */ 90 | 91 | /* Begin PBXResourcesBuildPhase section */ 92 | 7B64C02B0E61438200E4D136 /* Resources */ = { 93 | isa = PBXResourcesBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXResourcesBuildPhase section */ 100 | 101 | /* Begin PBXShellScriptBuildPhase section */ 102 | 7B64C02E0E61438200E4D136 /* ShellScript */ = { 103 | isa = PBXShellScriptBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | ); 107 | inputPaths = ( 108 | ); 109 | outputPaths = ( 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | shellPath = /bin/sh; 113 | shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; 114 | }; 115 | /* End PBXShellScriptBuildPhase section */ 116 | 117 | /* Begin PBXSourcesBuildPhase section */ 118 | 7B64C02C0E61438200E4D136 /* Sources */ = { 119 | isa = PBXSourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | 7B64C0370E61438700E4D136 /* RMModelObjectTest.m in Sources */, 123 | 7B64C0390E61438900E4D136 /* RMModelObject.mm in Sources */, 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | /* End PBXSourcesBuildPhase section */ 128 | 129 | /* Begin XCBuildConfiguration section */ 130 | 7B64C0100E61428C00E4D136 /* Debug */ = { 131 | isa = XCBuildConfiguration; 132 | buildSettings = { 133 | COPY_PHASE_STRIP = NO; 134 | }; 135 | name = Debug; 136 | }; 137 | 7B64C0110E61428C00E4D136 /* Release */ = { 138 | isa = XCBuildConfiguration; 139 | buildSettings = { 140 | COPY_PHASE_STRIP = YES; 141 | }; 142 | name = Release; 143 | }; 144 | 7B64C0330E61438200E4D136 /* Debug */ = { 145 | isa = XCBuildConfiguration; 146 | buildSettings = { 147 | ALWAYS_SEARCH_USER_PATHS = NO; 148 | ARCHS = "$(ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1)"; 149 | ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1 = "ppc i386 ppc64 x86_64"; 150 | COPY_PHASE_STRIP = NO; 151 | FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; 152 | GCC_DYNAMIC_NO_PIC = NO; 153 | GCC_ENABLE_FIX_AND_CONTINUE = NO; 154 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 155 | GCC_MODEL_TUNING = G5; 156 | GCC_OPTIMIZATION_LEVEL = 0; 157 | GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/Cocoa.framework/Headers/Cocoa.h"; 158 | GCC_PREPROCESSOR_DEFINITIONS = "NS_BUILD_32_LIKE_64=1"; 159 | INFOPLIST_FILE = "RMModelObjectTest-Info.plist"; 160 | INSTALL_PATH = "$(USER_LIBRARY_DIR)/Bundles"; 161 | OTHER_LDFLAGS = ( 162 | "-framework", 163 | Cocoa, 164 | "-framework", 165 | SenTestingKit, 166 | ); 167 | PREBINDING = NO; 168 | PRODUCT_NAME = RMModelObjectTest; 169 | WRAPPER_EXTENSION = octest; 170 | }; 171 | name = Debug; 172 | }; 173 | 7B64C0340E61438200E4D136 /* Release */ = { 174 | isa = XCBuildConfiguration; 175 | buildSettings = { 176 | ALWAYS_SEARCH_USER_PATHS = NO; 177 | ARCHS = "$(ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1)"; 178 | ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1 = "ppc i386 ppc64 x86_64"; 179 | COPY_PHASE_STRIP = YES; 180 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 181 | FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; 182 | GCC_ENABLE_FIX_AND_CONTINUE = NO; 183 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 184 | GCC_MODEL_TUNING = G5; 185 | GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/Cocoa.framework/Headers/Cocoa.h"; 186 | GCC_PREPROCESSOR_DEFINITIONS = "NS_BUILD_32_LIKE_64=1"; 187 | INFOPLIST_FILE = "RMModelObjectTest-Info.plist"; 188 | INSTALL_PATH = "$(USER_LIBRARY_DIR)/Bundles"; 189 | OTHER_LDFLAGS = ( 190 | "-framework", 191 | Cocoa, 192 | "-framework", 193 | SenTestingKit, 194 | ); 195 | PREBINDING = NO; 196 | PRODUCT_NAME = RMModelObjectTest; 197 | WRAPPER_EXTENSION = octest; 198 | ZERO_LINK = NO; 199 | }; 200 | name = Release; 201 | }; 202 | /* End XCBuildConfiguration section */ 203 | 204 | /* Begin XCConfigurationList section */ 205 | 7B64C0120E61428C00E4D136 /* Build configuration list for PBXProject "RMModelObject" */ = { 206 | isa = XCConfigurationList; 207 | buildConfigurations = ( 208 | 7B64C0100E61428C00E4D136 /* Debug */, 209 | 7B64C0110E61428C00E4D136 /* Release */, 210 | ); 211 | defaultConfigurationIsVisible = 0; 212 | defaultConfigurationName = Release; 213 | }; 214 | 7B64C0350E61438200E4D136 /* Build configuration list for PBXNativeTarget "RMModelObjectTest" */ = { 215 | isa = XCConfigurationList; 216 | buildConfigurations = ( 217 | 7B64C0330E61438200E4D136 /* Debug */, 218 | 7B64C0340E61438200E4D136 /* Release */, 219 | ); 220 | defaultConfigurationIsVisible = 0; 221 | defaultConfigurationName = Release; 222 | }; 223 | /* End XCConfigurationList section */ 224 | }; 225 | rootObject = 7B64C00F0E61428C00E4D136 /* Project object */; 226 | } 227 | -------------------------------------------------------------------------------- /RMModelObjectTest-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.yourcompany.${PRODUCT_NAME:identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleSignature 16 | ???? 17 | CFBundleVersion 18 | 1.0 19 | 20 | 21 | -------------------------------------------------------------------------------- /RMModelObjectTest.m: -------------------------------------------------------------------------------- 1 | //*************************************************************************** 2 | 3 | // Copyright (C) 2008 Realmac Software Ltd 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject 11 | // to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | // ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 21 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | //*************************************************************************** 25 | 26 | #import 27 | 28 | #import "RMModelObject.h" 29 | 30 | //*************************************************************************** 31 | 32 | @interface RMModelObjectTest : RMModelObject 33 | { 34 | int _synthesizedProperty; 35 | } 36 | 37 | // Primitive Types 38 | @property BOOL b; 39 | @property char c; 40 | @property unsigned char uc; 41 | @property int i; 42 | @property unsigned int ui; 43 | @property short s; 44 | @property unsigned short us; 45 | @property long l; 46 | @property unsigned long ul; 47 | @property long long ll; 48 | @property unsigned long long ull; 49 | @property float f; 50 | @property double d; 51 | 52 | // Typical Cocoa aggregate types 53 | @property NSRect nsrect; 54 | @property NSRange nsrange; 55 | @property NSPoint nspoint; 56 | @property NSSize nssize; 57 | 58 | @property int synthesizedProperty; 59 | 60 | // Objects 61 | @property (assign) NSString* assignedString; 62 | @property (retain) NSString* retainedString; 63 | @property (copy) NSString* copiedString; 64 | @property (retain, readonly) NSString* readOnlyString; 65 | @property (retain) NSSet* set; // Testing that 'set' should be a getter name, not a setter 66 | //@property (retain, getter=foo, setter=bar:) NSString* customAccessorString; // Custom getter/setter names not supported yet 67 | 68 | @end 69 | 70 | //*************************************************************************** 71 | 72 | @implementation RMModelObjectTest 73 | 74 | @synthesize synthesizedProperty = _synthesizedProperty; 75 | 76 | @dynamic b, c, uc, i, ui, s, us, l, ul, ll, ull, f, d, nsrect, nsrange, nspoint, nssize; 77 | @dynamic assignedString, retainedString, copiedString, readOnlyString, set; 78 | 79 | @end 80 | 81 | //*************************************************************************** 82 | 83 | @interface RMModelObjectTestCase : SenTestCase 84 | 85 | @end 86 | 87 | //--------------------------------------------------------------------------- 88 | 89 | @implementation RMModelObjectTestCase 90 | 91 | - (void)testModelObject 92 | { 93 | NSAutoreleasePool* pool = [NSAutoreleasePool new]; 94 | 95 | RMModelObjectTest* testObject = [[RMModelObjectTest alloc] init]; 96 | 97 | // 98 | 99 | STAssertEquals(testObject.i, 0, nil); 100 | 101 | testObject.i = 69; 102 | STAssertEquals(testObject.i, 69, nil); 103 | 104 | // 105 | 106 | STAssertEqualsWithAccuracy(testObject.f, 0.0f, FLT_EPSILON, nil); 107 | 108 | testObject.f = 1.618f; 109 | STAssertEqualsWithAccuracy(testObject.f, 1.618f, FLT_EPSILON, nil); 110 | 111 | // 112 | 113 | STAssertEqualsWithAccuracy(testObject.d, 0.0, DBL_EPSILON, nil); 114 | 115 | testObject.d = 71.0; 116 | STAssertEqualsWithAccuracy(testObject.d, 71.0, DBL_EPSILON, nil); 117 | 118 | // 119 | 120 | STAssertEquals(testObject.b, NO, nil); 121 | 122 | testObject.b = YES; 123 | STAssertEquals(testObject.b, YES, nil); 124 | 125 | // 126 | 127 | unsigned long long ll1 = testObject.ull; 128 | STAssertEquals(ll1, 0ULL, nil); 129 | 130 | testObject.ull = 18446744073709551610ULL; 131 | STAssertEquals(testObject.ull, 18446744073709551610ULL, nil); 132 | 133 | // 134 | 135 | NSString* fooString = [[NSString alloc] initWithUTF8String:"Foo"]; 136 | if(![NSGarbageCollector defaultCollector]) STAssertEquals([fooString retainCount], 1UL, nil); 137 | 138 | testObject.assignedString = fooString; 139 | STAssertEquals(testObject.assignedString, fooString, nil); 140 | STAssertEqualObjects(testObject.assignedString, fooString, nil); 141 | if(![NSGarbageCollector defaultCollector]) STAssertEquals([testObject.assignedString retainCount], 1UL, nil); 142 | 143 | // 144 | 145 | id fooStringId = [[NSString alloc] initWithUTF8String:"Foo"]; 146 | STAssertEqualObjects(testObject.assignedString, fooStringId, nil); 147 | 148 | // 149 | 150 | NSString* barString = [[NSString alloc] initWithUTF8String:"Bar"]; 151 | if(![NSGarbageCollector defaultCollector]) STAssertEquals([barString retainCount], 1UL, nil); 152 | 153 | testObject.retainedString = barString; 154 | STAssertEquals(testObject.retainedString, barString, nil); 155 | STAssertTrue(testObject.retainedString == barString, nil); 156 | if(![NSGarbageCollector defaultCollector]) STAssertEquals([barString retainCount], 4UL, nil); 157 | if(![NSGarbageCollector defaultCollector]) STAssertEquals([testObject.retainedString retainCount], 5UL, nil); 158 | 159 | // 160 | 161 | NSMutableString* bazString = [[NSMutableString alloc] initWithUTF8String:"Baz"]; 162 | testObject.copiedString = bazString; 163 | STAssertFalse(testObject.copiedString == bazString, nil); 164 | STAssertEqualObjects(testObject.copiedString, bazString, nil); 165 | 166 | // 167 | 168 | STAssertEqualObjects(testObject.set, nil, nil); 169 | 170 | testObject.set = [NSSet setWithObject:@"Hello"]; 171 | NSSet* anotherSet = [NSSet setWithObjects:@"Hello", nil]; 172 | STAssertEqualObjects(testObject.set, anotherSet, nil); 173 | 174 | RMModelObjectTest* copiedObject = [[testObject copy] autorelease]; 175 | STAssertTrue([testObject isEqual:copiedObject], nil); 176 | 177 | STAssertEquals(copiedObject.i, 69, nil); 178 | STAssertEquals(copiedObject.b, YES, nil); 179 | STAssertEquals(copiedObject.ull, 18446744073709551610ULL, nil); 180 | 181 | STAssertEquals(copiedObject.assignedString, fooString, nil); 182 | STAssertEqualObjects(copiedObject.assignedString, fooString, nil); 183 | if(![NSGarbageCollector defaultCollector]) STAssertEquals([copiedObject.assignedString retainCount], 1UL, nil); 184 | 185 | STAssertEquals(copiedObject.retainedString, barString, nil); 186 | if(![NSGarbageCollector defaultCollector]) STAssertTrue([copiedObject.retainedString retainCount] > 2UL, nil); 187 | 188 | STAssertFalse(copiedObject.copiedString == bazString, nil); 189 | STAssertEquals(copiedObject.copiedString, testObject.copiedString, nil); // Dependent on immutable -[NSString copy] simply bumping the retain count 190 | 191 | // 192 | 193 | Class archiverClass = [NSArchiver class]; 194 | for(; archiverClass != [NSKeyedArchiver class]; archiverClass = [NSKeyedArchiver class]) 195 | { 196 | Class unarchiverClass = (archiverClass == [NSKeyedArchiver class]) ? [NSKeyedUnarchiver class] : [NSUnarchiver class]; 197 | 198 | NSData* archivedData = [archiverClass archivedDataWithRootObject:copiedObject]; 199 | RMModelObjectTest* unarchivedObject = [unarchiverClass unarchiveObjectWithData:archivedData]; 200 | 201 | STAssertNotNil(unarchivedObject, nil); 202 | 203 | STAssertEquals(unarchivedObject.i, 69, nil); 204 | STAssertEquals(unarchivedObject.b, YES, nil); 205 | STAssertEquals(unarchivedObject.ull, 18446744073709551610ULL, nil); 206 | 207 | STAssertFalse(unarchivedObject.assignedString == fooString, nil); 208 | if([NSGarbageCollector defaultCollector]) STAssertEqualObjects(unarchivedObject.assignedString, fooString, nil); 209 | 210 | STAssertFalse(unarchivedObject.retainedString == barString, nil); 211 | STAssertEqualObjects(unarchivedObject.retainedString, barString, nil); 212 | 213 | STAssertFalse(unarchivedObject.copiedString == bazString, nil); 214 | STAssertEqualObjects(unarchivedObject.copiedString, bazString, nil); 215 | STAssertEqualObjects(unarchivedObject.copiedString, testObject.copiedString, nil); 216 | STAssertEqualObjects(unarchivedObject.copiedString, copiedObject.copiedString, nil); 217 | } 218 | 219 | // 220 | 221 | copiedObject.assignedString = nil; 222 | STAssertEquals(copiedObject.assignedString, (id)nil, nil); 223 | STAssertNotNil(testObject.assignedString, nil); 224 | 225 | copiedObject.retainedString = nil; 226 | STAssertEquals(copiedObject.retainedString, (id)nil, nil); 227 | STAssertNotNil(testObject.retainedString, nil); 228 | 229 | copiedObject.copiedString = nil; 230 | STAssertEquals(copiedObject.copiedString, (id)nil, nil); 231 | STAssertNotNil(testObject.copiedString, nil); 232 | 233 | // 234 | 235 | [fooString release]; 236 | [fooStringId release]; 237 | [barString release]; 238 | [bazString release]; 239 | 240 | // 241 | 242 | [pool drain]; 243 | } 244 | 245 | @end 246 | 247 | //*************************************************************************** 248 | --------------------------------------------------------------------------------