diff --git a/JSONKit.h b/JSONKit.h index 6e179ca..71bd0c3 100644 --- a/JSONKit.h +++ b/JSONKit.h @@ -126,10 +126,11 @@ enum { typedef JKFlags JKParseOptionFlags; enum { - JKSerializeOptionNone = 0, - JKSerializeOptionPretty = (1 << 0), - JKSerializeOptionEscapeUnicode = (1 << 1), - JKSerializeOptionValidFlags = (JKSerializeOptionPretty | JKSerializeOptionEscapeUnicode), + JKSerializeOptionNone = 0, + JKSerializeOptionPretty = (1 << 0), + JKSerializeOptionEscapeUnicode = (1 << 1), + JKSerializeOptionEscapeForwardSlashes = (1 << 4), + JKSerializeOptionValidFlags = (JKSerializeOptionPretty | JKSerializeOptionEscapeUnicode | JKSerializeOptionEscapeForwardSlashes), }; typedef JKFlags JKSerializeOptionFlags; diff --git a/JSONKit.m b/JSONKit.m index fda9e21..0e9331f 100644 --- a/JSONKit.m +++ b/JSONKit.m @@ -124,6 +124,10 @@ The code in isValidCodePoint() is derived from the ICU code in #import #import +#ifndef __has_feature +#define __has_feature(x) 0 +#endif + #ifdef JK_ENABLE_CF_TRANSFER_OWNERSHIP_CALLBACKS #warning As of JSONKit v1.4, JK_ENABLE_CF_TRANSFER_OWNERSHIP_CALLBACKS is no longer required. It is no longer a valid option. #endif @@ -132,6 +136,10 @@ The code in isValidCodePoint() is derived from the ICU code in #error JSONKit does not support Objective-C Garbage Collection #endif +#if __has_feature(objc_arc) +#error JSONKit does not support Objective-C Automatic Reference Counting (ARC) +#endif + // The following checks are really nothing more than sanity checks. // JSONKit technically has a few problems from a "strictly C99 conforming" standpoint, though they are of the pedantic nitpicking variety. // In practice, though, for the compilers and architectures we can reasonably expect this code to be compiled for, these pedantic nitpicks aren't really a problem. @@ -167,7 +175,7 @@ The code in isValidCodePoint() is derived from the ICU code in #define JK_CACHE_SLOTS (1UL << JK_CACHE_SLOTS_BITS) // JK_CACHE_PROBES is the number of probe attempts. #define JK_CACHE_PROBES (4UL) -// JK_INIT_CACHE_AGE must be (1 << AGE) - 1 +// JK_INIT_CACHE_AGE must be < (1 << AGE) - 1, where AGE is sizeof(typeof(AGE)) * 8. #define JK_INIT_CACHE_AGE (0) // JK_TOKENBUFFER_SIZE is the default stack size for the temporary buffer used to hold "non-simple" strings (i.e., contains \ escapes) @@ -601,7 +609,7 @@ - (void)releaseState; JK_STATIC_INLINE size_t jk_min(size_t a, size_t b); JK_STATIC_INLINE size_t jk_max(size_t a, size_t b); -JK_STATIC_INLINE JKHash calculateHash(JKHash currentHash, unsigned char c); +JK_STATIC_INLINE JKHash jk_calculateHash(JKHash currentHash, unsigned char c); // JSONKit v1.4 used both a JKArray : NSArray and JKMutableArray : NSMutableArray, and the same for the dictionary collection type. // However, Louis Gerbarg (via cocoa-dev) pointed out that Cocoa / Core Foundation actually implements only a single class that inherits from the @@ -609,6 +617,46 @@ - (void)releaseState; // classes receive the mutating methods, but this is handled by having those methods throw an exception when the ivar bit is set to immutable. // We adopt the same strategy here. It's both cleaner and gets rid of the method swizzling hackery used in JSONKit v1.4. + +// This is a workaround for issue #23 https://github.com/johnezang/JSONKit/pull/23 +// Basically, there seem to be a problem with using +load in static libraries on iOS. However, __attribute__ ((constructor)) does work correctly. +// Since we do not require anything "special" that +load provides, and we can accomplish the same thing using __attribute__ ((constructor)), the +load logic was moved here. + +static Class _JKArrayClass = NULL; +static size_t _JKArrayInstanceSize = 0UL; +static Class _JKDictionaryClass = NULL; +static size_t _JKDictionaryInstanceSize = 0UL; + +// For JSONDecoder... +static Class _jk_NSNumberClass = NULL; +static NSNumberAllocImp _jk_NSNumberAllocImp = NULL; +static NSNumberInitWithUnsignedLongLongImp _jk_NSNumberInitWithUnsignedLongLongImp = NULL; + +extern void jk_collectionClassLoadTimeInitialization(void) __attribute__ ((constructor)); + +void jk_collectionClassLoadTimeInitialization(void) { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Though technically not required, the run time environment at load time initialization may be less than ideal. + + _JKArrayClass = objc_getClass("JKArray"); + _JKArrayInstanceSize = jk_max(16UL, class_getInstanceSize(_JKArrayClass)); + + _JKDictionaryClass = objc_getClass("JKDictionary"); + _JKDictionaryInstanceSize = jk_max(16UL, class_getInstanceSize(_JKDictionaryClass)); + + // For JSONDecoder... + _jk_NSNumberClass = [NSNumber class]; + _jk_NSNumberAllocImp = (NSNumberAllocImp)[NSNumber methodForSelector:@selector(alloc)]; + + // Hacktacular. Need to do it this way due to the nature of class clusters. + id temp_NSNumber = [NSNumber alloc]; + _jk_NSNumberInitWithUnsignedLongLongImp = (NSNumberInitWithUnsignedLongLongImp)[temp_NSNumber methodForSelector:@selector(initWithUnsignedLongLong:)]; + [[temp_NSNumber init] release]; + temp_NSNumber = NULL; + + [pool release]; pool = NULL; +} + + #pragma mark - @interface JKArray : NSMutableArray { id *objects; @@ -618,19 +666,6 @@ @interface JKArray : NSMutableArray objects != NULL) && (array->count <= array->capacity) && (objectIndex <= array->count) && (newObject != NULL)); if(!((array != NULL) && (array->objects != NULL) && (objectIndex <= array->count) && (newObject != NULL))) { [newObject autorelease]; return; } - array->count++; - if(array->count >= array->capacity) { - array->capacity += 16UL; + if((array->count + 1UL) >= array->capacity) { id *newObjects = NULL; - if((newObjects = (id *)realloc(array->objects, sizeof(id) * array->capacity)) == NULL) { [NSException raise:NSMallocException format:@"Unable to resize objects array."]; } + if((newObjects = (id *)realloc(array->objects, sizeof(id) * (array->capacity + 16UL))) == NULL) { [NSException raise:NSMallocException format:@"Unable to resize objects array."]; } array->objects = newObjects; + array->capacity += 16UL; memset(&array->objects[array->count], 0, sizeof(id) * (array->capacity - array->count)); } + array->count++; if((objectIndex + 1UL) < array->count) { memmove(&array->objects[objectIndex + 1UL], &array->objects[objectIndex], sizeof(id) * ((array->count - 1UL) - objectIndex)); array->objects[objectIndex] = NULL; } array->objects[objectIndex] = newObject; } @@ -679,11 +714,11 @@ static void _JKArrayReplaceObjectAtIndexWithObject(JKArray *array, NSUInteger ob } static void _JKArrayRemoveObjectAtIndex(JKArray *array, NSUInteger objectIndex) { - NSCParameterAssert((array != NULL) && (array->objects != NULL) && (array->count <= array->capacity) && (objectIndex < array->count) && (array->objects[objectIndex] != NULL)); - if(!((array != NULL) && (array->objects != NULL) && (objectIndex < array->count) && (array->objects[objectIndex] != NULL))) { return; } + NSCParameterAssert((array != NULL) && (array->objects != NULL) && (array->count > 0UL) && (array->count <= array->capacity) && (objectIndex < array->count) && (array->objects[objectIndex] != NULL)); + if(!((array != NULL) && (array->objects != NULL) && (array->count > 0UL) && (array->count <= array->capacity) && (objectIndex < array->count) && (array->objects[objectIndex] != NULL))) { return; } CFRelease(array->objects[objectIndex]); array->objects[objectIndex] = NULL; - if((objectIndex + 1UL) < array->count) { memmove(&array->objects[objectIndex], &array->objects[objectIndex + 1UL], sizeof(id) * ((array->count - 1UL) - objectIndex)); array->objects[array->count] = NULL; } + if((objectIndex + 1UL) < array->count) { memmove(&array->objects[objectIndex], &array->objects[objectIndex + 1UL], sizeof(id) * ((array->count - 1UL) - objectIndex)); array->objects[array->count - 1UL] = NULL; } array->count--; } @@ -707,14 +742,16 @@ - (NSUInteger)count - (void)getObjects:(id *)objectsPtr range:(NSRange)range { NSParameterAssert((objects != NULL) && (count <= capacity)); - if((objectsPtr == NULL) && (NSMaxRange(range) > 0UL)) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: pointer to objects array is NULL but range length is %lu", NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSMaxRange(range)]; } - if((range.location > count) || (NSMaxRange(range) > count)) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSMaxRange(range), count]; } + if((objectsPtr == NULL) && (NSMaxRange(range) > 0UL)) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: pointer to objects array is NULL but range length is %lu", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (unsigned long)NSMaxRange(range)]; } + if((range.location > count) || (NSMaxRange(range) > count)) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (unsigned long)NSMaxRange(range), (unsigned long)count]; } +#ifndef __clang_analyzer__ memcpy(objectsPtr, objects + range.location, range.length * sizeof(id)); +#endif } - (id)objectAtIndex:(NSUInteger)objectIndex { - if(objectIndex >= count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), objectIndex, count]; } + if(objectIndex >= count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (unsigned long)objectIndex, (unsigned long)count]; } NSParameterAssert((objects != NULL) && (count <= capacity) && (objects[objectIndex] != NULL)); return(objects[objectIndex]); } @@ -735,8 +772,12 @@ - (void)insertObject:(id)anObject atIndex:(NSUInteger)objectIndex { if(mutations == 0UL) { [NSException raise:NSInternalInconsistencyException format:@"*** -[%@ %@]: mutating method sent to immutable object", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; } if(anObject == NULL) { [NSException raise:NSInvalidArgumentException format:@"*** -[%@ %@]: attempt to insert nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; } - if(objectIndex > count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), objectIndex, count + 1UL]; } + if(objectIndex > count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (unsigned long)objectIndex, (unsigned long)(count + 1UL)]; } +#ifdef __clang_analyzer__ + [anObject retain]; // Stupid clang analyzer... Issue #19. +#else anObject = [anObject retain]; +#endif _JKArrayInsertObjectAtIndex(self, anObject, objectIndex); mutations = (mutations == NSUIntegerMax) ? 1UL : mutations + 1UL; } @@ -744,7 +785,7 @@ - (void)insertObject:(id)anObject atIndex:(NSUInteger)objectIndex - (void)removeObjectAtIndex:(NSUInteger)objectIndex { if(mutations == 0UL) { [NSException raise:NSInternalInconsistencyException format:@"*** -[%@ %@]: mutating method sent to immutable object", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; } - if(objectIndex >= count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), objectIndex, count]; } + if(objectIndex >= count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (unsigned long)objectIndex, (unsigned long)count]; } _JKArrayRemoveObjectAtIndex(self, objectIndex); mutations = (mutations == NSUIntegerMax) ? 1UL : mutations + 1UL; } @@ -753,8 +794,12 @@ - (void)replaceObjectAtIndex:(NSUInteger)objectIndex withObject:(id)anObject { if(mutations == 0UL) { [NSException raise:NSInternalInconsistencyException format:@"*** -[%@ %@]: mutating method sent to immutable object", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; } if(anObject == NULL) { [NSException raise:NSInvalidArgumentException format:@"*** -[%@ %@]: attempt to insert nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; } - if(objectIndex >= count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), objectIndex, count]; } + if(objectIndex >= count) { [NSException raise:NSRangeException format:@"*** -[%@ %@]: index (%lu) beyond bounds (%lu)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (unsigned long)objectIndex, (unsigned long)count]; } +#ifdef __clang_analyzer__ + [anObject retain]; // Stupid clang analyzer... Issue #19. +#else anObject = [anObject retain]; +#endif _JKArrayReplaceObjectAtIndexWithObject(self, objectIndex, anObject); mutations = (mutations == NSUIntegerMax) ? 1UL : mutations + 1UL; } @@ -762,13 +807,13 @@ - (void)replaceObjectAtIndex:(NSUInteger)objectIndex withObject:(id)anObject - (id)copyWithZone:(NSZone *)zone { NSParameterAssert((objects != NULL) && (count <= capacity)); - return((mutations == 0UL) ? [self retain] : [[NSArray allocWithZone:zone] initWithObjects:objects count:count]); + return((mutations == 0UL) ? [self retain] : [(NSArray *)[NSArray allocWithZone:zone] initWithObjects:objects count:count]); } - (id)mutableCopyWithZone:(NSZone *)zone { NSParameterAssert((objects != NULL) && (count <= capacity)); - return([[NSMutableArray allocWithZone:zone] initWithObjects:objects count:count]); + return([(NSMutableArray *)[NSMutableArray allocWithZone:zone] initWithObjects:objects count:count]); } @end @@ -805,7 +850,7 @@ - (void)dealloc - (NSArray *)allObjects { NSParameterAssert(collection != NULL); - NSUInteger count = [collection count], atObject = 0UL; + NSUInteger count = [(NSDictionary *)collection count], atObject = 0UL; id objects[count]; while((objects[atObject] = [self nextObject]) != NULL) { NSParameterAssert(atObject < count); atObject++; } @@ -836,19 +881,6 @@ @interface JKDictionary : NSMutableDictionary bottom) { mid = (top + bottom) / 2UL; if(jk_dictionaryCapacities[mid] < tableSize) { bottom = mid + 1UL; } else { top = mid; } } return(jk_dictionaryCapacities[bottom]); } @@ -937,11 +969,30 @@ static NSUInteger _JKDictionaryCapacity(JKDictionary *dictionary) { } static void _JKDictionaryRemoveObjectWithEntry(JKDictionary *dictionary, JKHashTableEntry *entry) { - NSCParameterAssert((dictionary != NULL) && (entry != NULL) && (entry->key != NULL) && (entry->object != NULL) && (dictionary->count > 0UL)); + NSCParameterAssert((dictionary != NULL) && (entry != NULL) && (entry->key != NULL) && (entry->object != NULL) && (dictionary->count > 0UL) && (dictionary->count <= dictionary->capacity)); CFRelease(entry->key); entry->key = NULL; CFRelease(entry->object); entry->object = NULL; entry->keyHash = 0UL; dictionary->count--; + // In order for certain invariants that are used to speed up the search for a particular key, we need to "re-add" all the entries in the hash table following this entry until we hit a NULL entry. + NSUInteger removeIdx = entry - dictionary->entry, idx = 0UL; + NSCParameterAssert((removeIdx < dictionary->capacity)); + for(idx = 0UL; idx < dictionary->capacity; idx++) { + NSUInteger entryIdx = (removeIdx + idx + 1UL) % dictionary->capacity; + JKHashTableEntry *atEntry = &dictionary->entry[entryIdx]; + if(atEntry->key == NULL) { break; } + NSUInteger keyHash = atEntry->keyHash; + id key = atEntry->key, object = atEntry->object; + NSCParameterAssert(object != NULL); + atEntry->keyHash = 0UL; + atEntry->key = NULL; + atEntry->object = NULL; + NSUInteger addKeyEntry = keyHash % dictionary->capacity, addIdx = 0UL; + for(addIdx = 0UL; addIdx < dictionary->capacity; addIdx++) { + JKHashTableEntry *atAddEntry = &dictionary->entry[((addKeyEntry + addIdx) % dictionary->capacity)]; + if(JK_EXPECT_T(atAddEntry->key == NULL)) { NSCParameterAssert((atAddEntry->keyHash == 0UL) && (atAddEntry->object == NULL)); atAddEntry->key = key; atAddEntry->object = object; atAddEntry->keyHash = keyHash; break; } + } + } } static void _JKDictionaryAddObject(JKDictionary *dictionary, NSUInteger keyHash, id key, id object) { @@ -951,7 +1002,7 @@ static void _JKDictionaryAddObject(JKDictionary *dictionary, NSUInteger keyHash, NSUInteger entryIdx = (keyEntry + idx) % dictionary->capacity; JKHashTableEntry *atEntry = &dictionary->entry[entryIdx]; if(JK_EXPECT_F(atEntry->keyHash == keyHash) && JK_EXPECT_T(atEntry->key != NULL) && (JK_EXPECT_F(key == atEntry->key) || JK_EXPECT_F(CFEqual(atEntry->key, key)))) { _JKDictionaryRemoveObjectWithEntry(dictionary, atEntry); } - if(JK_EXPECT_T(atEntry->key == NULL)) { atEntry->key = key; atEntry->object = object; atEntry->keyHash = keyHash; dictionary->count++; return; } + if(JK_EXPECT_T(atEntry->key == NULL)) { NSCParameterAssert((atEntry->keyHash == 0UL) && (atEntry->object == NULL)); atEntry->key = key; atEntry->object = object; atEntry->keyHash = keyHash; dictionary->count++; return; } } // We should never get here. If we do, we -release the key / object because it's our responsibility. @@ -1062,7 +1113,8 @@ - (id)mutableCopyWithZone:(NSZone *)zone JK_STATIC_INLINE size_t jk_min(size_t a, size_t b) { return((a < b) ? a : b); } JK_STATIC_INLINE size_t jk_max(size_t a, size_t b) { return((a > b) ? a : b); } -JK_STATIC_INLINE JKHash calculateHash(JKHash currentHash, unsigned char c) { return(((currentHash << 5) + currentHash) + c); } +JK_STATIC_INLINE JKHash jk_calculateHash(JKHash currentHash, unsigned char c) { return((((currentHash << 5) + currentHash) + (c - 29)) ^ (currentHash >> 19)); } + static void jk_error(JKParseState *parseState, NSString *format, ...) { NSCParameterAssert((parseState != NULL) && (format != NULL)); @@ -1359,7 +1411,7 @@ JK_STATIC_INLINE int jk_string_add_unicodeCodePoint(JKParseState *parseState, ui if((result = ConvertUTF32toUTF8(unicodeCodePoint, &u8s, (parseState->token.tokenBuffer.bytes.ptr + parseState->token.tokenBuffer.bytes.length))) != conversionOK) { if(result == targetExhausted) { return(1); } } size_t utf8len = u8s - &parseState->token.tokenBuffer.bytes.ptr[*tokenBufferIdx], nextIdx = (*tokenBufferIdx) + utf8len; - while(*tokenBufferIdx < nextIdx) { *stringHash = calculateHash(*stringHash, parseState->token.tokenBuffer.bytes.ptr[(*tokenBufferIdx)++]); } + while(*tokenBufferIdx < nextIdx) { *stringHash = jk_calculateHash(*stringHash, parseState->token.tokenBuffer.bytes.ptr[(*tokenBufferIdx)++]); } return(0); } @@ -1393,8 +1445,8 @@ static int jk_parse_string(JKParseState *parseState) { ConversionResult result; if(JK_EXPECT_F((result = ConvertSingleCodePointInUTF8(atStringCharacter - 1, endOfBuffer, (UTF8 const **)&nextValidCharacter, &u32ch)) != conversionOK)) { goto switchToSlowPath; } - stringHash = calculateHash(stringHash, currentChar); - while(atStringCharacter < nextValidCharacter) { NSCParameterAssert(JK_AT_STRING_PTR(parseState) <= JK_END_STRING_PTR(parseState)); stringHash = calculateHash(stringHash, *atStringCharacter++); } + stringHash = jk_calculateHash(stringHash, currentChar); + while(atStringCharacter < nextValidCharacter) { NSCParameterAssert(JK_AT_STRING_PTR(parseState) <= JK_END_STRING_PTR(parseState)); stringHash = jk_calculateHash(stringHash, *atStringCharacter++); } continue; } else { if(JK_EXPECT_F(currentChar == (unsigned long)'"')) { stringState = JSONStringStateFinished; goto finishedParsing; } @@ -1411,7 +1463,7 @@ static int jk_parse_string(JKParseState *parseState) { if(JK_EXPECT_F(currentChar < 0x20UL)) { jk_error(parseState, @"Invalid character < 0x20 found in string: 0x%2.2x.", currentChar); stringState = JSONStringStateError; goto finishedParsing; } - stringHash = calculateHash(stringHash, currentChar); + stringHash = jk_calculateHash(stringHash, currentChar); } } @@ -1429,7 +1481,7 @@ static int jk_parse_string(JKParseState *parseState) { if(JK_EXPECT_T(currentChar < (unsigned long)0x80)) { // Not a UTF8 sequence if(JK_EXPECT_F(currentChar == (unsigned long)'"')) { stringState = JSONStringStateFinished; atStringCharacter++; goto finishedParsing; } if(JK_EXPECT_F(currentChar == (unsigned long)'\\')) { stringState = JSONStringStateEscape; continue; } - stringHash = calculateHash(stringHash, currentChar); + stringHash = jk_calculateHash(stringHash, currentChar); tokenBuffer[tokenBufferIdx++] = currentChar; continue; } else { // UTF8 sequence @@ -1444,7 +1496,7 @@ static int jk_parse_string(JKParseState *parseState) { atStringCharacter = nextValidCharacter - 1; continue; } else { - while(atStringCharacter < nextValidCharacter) { tokenBuffer[tokenBufferIdx++] = *atStringCharacter; stringHash = calculateHash(stringHash, *atStringCharacter++); } + while(atStringCharacter < nextValidCharacter) { tokenBuffer[tokenBufferIdx++] = *atStringCharacter; stringHash = jk_calculateHash(stringHash, *atStringCharacter++); } atStringCharacter--; continue; } @@ -1472,7 +1524,7 @@ static int jk_parse_string(JKParseState *parseState) { parsedEscapedChar: stringState = JSONStringStateParsing; - stringHash = calculateHash(stringHash, escapedChar); + stringHash = jk_calculateHash(stringHash, escapedChar); tokenBuffer[tokenBufferIdx++] = escapedChar; break; @@ -1532,7 +1584,7 @@ static int jk_parse_string(JKParseState *parseState) { break; case JSONStringStateEscapedNeedEscapeForSurrogate: - if((currentChar == '\\')) { stringState = JSONStringStateEscapedNeedEscapedUForSurrogate; } + if(currentChar == '\\') { stringState = JSONStringStateEscapedNeedEscapedUForSurrogate; } else { if((parseState->parseOptionFlags & JKParseOptionLooseUnicode) == 0) { jk_error(parseState, @"Required a second \\u Unicode escape sequence following a surrogate \\u Unicode escape sequence."); stringState = JSONStringStateError; goto finishedParsing; } else { stringState = JSONStringStateParsing; atStringCharacter--; if(jk_string_add_unicodeCodePoint(parseState, UNI_REPLACEMENT_CHAR, &tokenBufferIdx, &stringHash)) { jk_error(parseState, @"Internal error: Unable to add UTF8 sequence to internal string buffer. %@ line #%ld", [NSString stringWithUTF8String:__FILE__], (long)__LINE__); stringState = JSONStringStateError; goto finishedParsing; } } @@ -1660,7 +1712,7 @@ static int jk_parse_number(JKParseState *parseState) { if(JK_EXPECT_F(endOfNumber != &numberTempBuf[parseState->token.tokenPtrRange.length]) && JK_EXPECT_F(numberState != JSONNumberStateError)) { numberState = JSONNumberStateError; jk_error(parseState, @"The conversion function did not consume all of the number tokens characters."); } size_t hashIndex = 0UL; - for(hashIndex = 0UL; hashIndex < parseState->token.value.ptrRange.length; hashIndex++) { parseState->token.value.hash = calculateHash(parseState->token.value.hash, parseState->token.value.ptrRange.ptr[hashIndex]); } + for(hashIndex = 0UL; hashIndex < parseState->token.value.ptrRange.length; hashIndex++) { parseState->token.value.hash = jk_calculateHash(parseState->token.value.hash, parseState->token.value.ptrRange.ptr[hashIndex]); } } if(JK_EXPECT_F(numberState != JSONNumberStateFinished)) { jk_error(parseState, @"Invalid number."); } @@ -1923,7 +1975,7 @@ static id json_parse_it(JKParseState *parseState) { #pragma mark Object cache // This uses a Galois Linear Feedback Shift Register (LFSR) PRNG to pick which item in the cache to age. It has a period of (2^32)-1. -// NOTE: A LFSR *MUST* be initialized to a non-zero value and must always have a non-zero value. +// NOTE: A LFSR *MUST* be initialized to a non-zero value and must always have a non-zero value. The LFSR is initalized to 1 in -initWithParseOptions: JK_STATIC_INLINE void jk_cache_age(JKParseState *parseState) { NSCParameterAssert((parseState != NULL) && (parseState->cache.prng_lfsr != 0U)); parseState->cache.prng_lfsr = (parseState->cache.prng_lfsr >> 1) ^ ((0U - (parseState->cache.prng_lfsr & 1U)) & 0x80200003U); @@ -1934,9 +1986,8 @@ JK_STATIC_INLINE void jk_cache_age(JKParseState *parseState) { // // The hash table is a linear C array of JKTokenCacheItem. The terms "item" and "bucket" are synonymous with the index in to the cache array, i.e. cache.items[bucket]. // -// Items in the cache have an age associated with them. The age is the number of rightmost 1 bits, i.e. 0000 = 0, 0001 = 1, 0011 = 2, 0111 = 3, 1111 = 4. -// This allows us to use left and right shifts to add or subtract from an items age. Add = (age << 1) | 1. Subtract = age >> 0. Subtract is synonymous with "age" (i.e., age an item). -// The reason for this is it allows us to perform saturated adds and subtractions and is branchless. +// Items in the cache have an age associated with them. An items age is incremented using saturating unsigned arithmetic and decremeted using unsigned right shifts. +// Thus, an items age is managed using an AIMD policy- additive increase, multiplicative decrease. All age calculations and manipulations are branchless. // The primitive C type MUST be unsigned. It is currently a "char", which allows (at a minimum and in practice) 8 bits. // // A "useable bucket" is a bucket that is not in use (never populated), or has an age == 0. @@ -1951,12 +2002,12 @@ JK_STATIC_INLINE void jk_cache_age(JKParseState *parseState) { void *parsedAtom = NULL; if(JK_EXPECT_F(parseState->token.value.ptrRange.length == 0UL) && JK_EXPECT_T(parseState->token.value.type == JKValueTypeString)) { return(@""); } - + for(x = 0UL; x < JK_CACHE_PROBES; x++) { if(JK_EXPECT_F(parseState->cache.items[bucket].object == NULL)) { setBucket = 1UL; useableBucket = bucket; break; } - if((JK_EXPECT_T(parseState->cache.items[bucket].hash == parseState->token.value.hash)) && (JK_EXPECT_T(parseState->cache.items[bucket].size == parseState->token.value.ptrRange.length)) && (JK_EXPECT_T(parseState->cache.items[bucket].type == parseState->token.value.type)) && (JK_EXPECT_T(parseState->cache.items[bucket].bytes != NULL)) && (JK_EXPECT_T(strncmp((const char *)parseState->cache.items[bucket].bytes, (const char *)parseState->token.value.ptrRange.ptr, parseState->token.value.ptrRange.length) == 0U))) { - parseState->cache.age[bucket] = (parseState->cache.age[bucket] << 1) | 1U; + if((JK_EXPECT_T(parseState->cache.items[bucket].hash == parseState->token.value.hash)) && (JK_EXPECT_T(parseState->cache.items[bucket].size == parseState->token.value.ptrRange.length)) && (JK_EXPECT_T(parseState->cache.items[bucket].type == parseState->token.value.type)) && (JK_EXPECT_T(parseState->cache.items[bucket].bytes != NULL)) && (JK_EXPECT_T(memcmp(parseState->cache.items[bucket].bytes, parseState->token.value.ptrRange.ptr, parseState->token.value.ptrRange.length) == 0U))) { + parseState->cache.age[bucket] = (((uint32_t)parseState->cache.age[bucket]) + 1U) - (((((uint32_t)parseState->cache.age[bucket]) + 1U) >> 31) ^ 1U); parseState->token.value.cacheItem = &parseState->cache.items[bucket]; NSCParameterAssert(parseState->cache.items[bucket].object != NULL); return((void *)CFRetain(parseState->cache.items[bucket].object)); @@ -2026,26 +2077,6 @@ JK_STATIC_INLINE void jk_cache_age(JKParseState *parseState) { #pragma mark - @implementation JSONDecoder -static Class _jk_NSNumberClass; -static NSNumberAllocImp _jk_NSNumberAllocImp; -static NSNumberInitWithUnsignedLongLongImp _jk_NSNumberInitWithUnsignedLongLongImp; - -+ (void)load -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Though technically not required, the run time environment at +load time may be less than ideal. - - _jk_NSNumberClass = [NSNumber class]; - _jk_NSNumberAllocImp = (NSNumberAllocImp)[NSNumber methodForSelector:@selector(alloc)]; - - // Hacktacular. Need to do it this way due to the nature of class clusters. - id temp_NSNumber = [NSNumber alloc]; - _jk_NSNumberInitWithUnsignedLongLongImp = (NSNumberInitWithUnsignedLongLongImp)[temp_NSNumber methodForSelector:@selector(initWithUnsignedLongLong:)]; - [[temp_NSNumber init] release]; - temp_NSNumber = NULL; - - [pool release]; pool = NULL; -} - + (id)decoder { return([self decoderWithParseOptions:JKParseOptionStrict]); @@ -2525,23 +2556,61 @@ static int jk_encode_add_atom_to_buffer(JKEncodeState *encodeState, void *object return(0); } -#if 0 // When we encounter a class that we do not handle, and we have either a delegate or block that the user supplied to format unsupported classes, // we "re-run" the object check. However, we re-run the object check exactly ONCE. If the user supplies an object that isn't one of the - // supported classes, we fail the second type (i.e., double fault error). + // supported classes, we fail the second time (i.e., double fault error). BOOL rerunningAfterClassFormatter = NO; -rerunAfterClassFormatter: - if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.stringClass)) { isClass = JKClassString; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.numberClass)) { isClass = JKClassNumber; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.dictionaryClass)) { isClass = JKClassDictionary; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.arrayClass)) { isClass = JKClassArray; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.nullClass)) { isClass = JKClassNull; } + rerunAfterClassFormatter:; + + // XXX XXX XXX XXX + // + // We need to work around a bug in 10.7, which breaks ABI compatibility with Objective-C going back not just to 10.0, but OpenStep and even NextStep. + // + // It has long been documented that "the very first thing that a pointer to an Objective-C object "points to" is a pointer to that objects class". + // + // This is euphemistically called "tagged pointers". There are a number of highly technical problems with this, most involving long passages from + // the C standard(s). In short, one can make a strong case, couched from the perspective of the C standard(s), that that 10.7 "tagged pointers" are + // fundamentally Wrong and Broken, and should have never been implemented. Assuming those points are glossed over, because the change is very clearly + // breaking ABI compatibility, this should have resulted in a minimum of a "minimum version required" bump in various shared libraries to prevent + // causes code that used to work just fine to suddenly break without warning. + // + // In fact, the C standard says that the hack below is "undefined behavior"- there is no requirement that the 10.7 tagged pointer hack of setting the + // "lower, unused bits" must be preserved when casting the result to an integer type, but this "works" because for most architectures + // `sizeof(long) == sizeof(void *)` and the compiler uses the same representation for both. (note: this is informal, not meant to be + // normative or pedantically correct). + // + // In other words, while this "works" for now, technically the compiler is not obligated to do "what we want", and a later version of the compiler + // is not required in any way to produce the same results or behavior that earlier versions of the compiler did for the statement below. + // + // Fan-fucking-tastic. + // + // Why not just use `object_getClass()`? Because `object->isa` reduces to (typically) a *single* instruction. Calling `object_getClass()` requires + // that the compiler potentially spill registers, establish a function call frame / environment, and finally execute a "jump subroutine" instruction. + // Then, the called subroutine must spend half a dozen instructions in its prolog, however many instructions doing whatever it does, then half a dozen + // instructions in its prolog. One instruction compared to dozens, maybe a hundred instructions. + // + // Yes, that's one to two orders of magnitude difference. Which is compelling in its own right. When going for performance, you're often happy with + // gains in the two to three percent range. + // + // XXX XXX XXX XXX + + + BOOL workAroundMacOSXABIBreakingBug = (JK_EXPECT_F(((NSUInteger)object) & 0x1)) ? YES : NO; + void *objectISA = (JK_EXPECT_F(workAroundMacOSXABIBreakingBug)) ? NULL : *((void **)objectPtr); + if(JK_EXPECT_F(workAroundMacOSXABIBreakingBug)) { goto slowClassLookup; } + + if(JK_EXPECT_T(objectISA == encodeState->fastClassLookup.stringClass)) { isClass = JKClassString; } + else if(JK_EXPECT_T(objectISA == encodeState->fastClassLookup.numberClass)) { isClass = JKClassNumber; } + else if(JK_EXPECT_T(objectISA == encodeState->fastClassLookup.dictionaryClass)) { isClass = JKClassDictionary; } + else if(JK_EXPECT_T(objectISA == encodeState->fastClassLookup.arrayClass)) { isClass = JKClassArray; } + else if(JK_EXPECT_T(objectISA == encodeState->fastClassLookup.nullClass)) { isClass = JKClassNull; } else { - if(JK_EXPECT_T([object isKindOfClass:[NSString class]])) { encodeState->fastClassLookup.stringClass = object->isa; isClass = JKClassString; } - else if(JK_EXPECT_T([object isKindOfClass:[NSNumber class]])) { encodeState->fastClassLookup.numberClass = object->isa; isClass = JKClassNumber; } - else if(JK_EXPECT_T([object isKindOfClass:[NSDictionary class]])) { encodeState->fastClassLookup.dictionaryClass = object->isa; isClass = JKClassDictionary; } - else if(JK_EXPECT_T([object isKindOfClass:[NSArray class]])) { encodeState->fastClassLookup.arrayClass = object->isa; isClass = JKClassArray; } - else if(JK_EXPECT_T([object isKindOfClass:[NSNull class]])) { encodeState->fastClassLookup.nullClass = object->isa; isClass = JKClassNull; } + slowClassLookup: + if(JK_EXPECT_T([object isKindOfClass:[NSString class]])) { if(workAroundMacOSXABIBreakingBug == NO) { encodeState->fastClassLookup.stringClass = objectISA; } isClass = JKClassString; } + else if(JK_EXPECT_T([object isKindOfClass:[NSNumber class]])) { if(workAroundMacOSXABIBreakingBug == NO) { encodeState->fastClassLookup.numberClass = objectISA; } isClass = JKClassNumber; } + else if(JK_EXPECT_T([object isKindOfClass:[NSDictionary class]])) { if(workAroundMacOSXABIBreakingBug == NO) { encodeState->fastClassLookup.dictionaryClass = objectISA; } isClass = JKClassDictionary; } + else if(JK_EXPECT_T([object isKindOfClass:[NSArray class]])) { if(workAroundMacOSXABIBreakingBug == NO) { encodeState->fastClassLookup.arrayClass = objectISA; } isClass = JKClassArray; } + else if(JK_EXPECT_T([object isKindOfClass:[NSNull class]])) { if(workAroundMacOSXABIBreakingBug == NO) { encodeState->fastClassLookup.nullClass = objectISA; } isClass = JKClassNull; } else { if((rerunningAfterClassFormatter == NO) && ( #ifdef __BLOCKS__ @@ -2553,39 +2622,7 @@ static int jk_encode_add_atom_to_buffer(JKEncodeState *encodeState, void *object else { jk_encode_error(encodeState, @"Unable to serialize object class %@ that was returned by the unsupported class formatter. Original object class was %@.", (object == NULL) ? @"NULL" : NSStringFromClass([object class]), NSStringFromClass([encodeCacheObject class])); return(1); } } } -#else - - // When we encounter a class that we do not handle, and we have either a delegate or block that the user supplied to format unsupported classes, - // we "re-run" the object check. However, we re-run the object check exactly ONCE. If the user supplies an object that isn't one of the - // supported classes, we fail the second type (i.e., double fault error). - //BOOL rerunningAfterClassFormatter = NO; -//rerunAfterClassFormatter: - if( -#ifdef __BLOCKS__ - ((encodeState->classFormatterBlock) && ((object = encodeState->classFormatterBlock(object)) == NULL)) || -#endif - ((encodeState->classFormatterIMP) && ((object = encodeState->classFormatterIMP(encodeState->classFormatterDelegate, encodeState->classFormatterSelector, object)) == NULL)) ) { goto formatterError; } - - if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.stringClass)) { isClass = JKClassString; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.numberClass)) { isClass = JKClassNumber; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.dictionaryClass)) { isClass = JKClassDictionary; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.arrayClass)) { isClass = JKClassArray; } - else if(JK_EXPECT_T(object->isa == encodeState->fastClassLookup.nullClass)) { isClass = JKClassNull; } - else { - if(JK_EXPECT_T([object isKindOfClass:[NSString class]])) { encodeState->fastClassLookup.stringClass = object->isa; isClass = JKClassString; } - else if(JK_EXPECT_T([object isKindOfClass:[NSNumber class]])) { encodeState->fastClassLookup.numberClass = object->isa; isClass = JKClassNumber; } - else if(JK_EXPECT_T([object isKindOfClass:[NSDictionary class]])) { encodeState->fastClassLookup.dictionaryClass = object->isa; isClass = JKClassDictionary; } - else if(JK_EXPECT_T([object isKindOfClass:[NSArray class]])) { encodeState->fastClassLookup.arrayClass = object->isa; isClass = JKClassArray; } - else if(JK_EXPECT_T([object isKindOfClass:[NSNull class]])) { encodeState->fastClassLookup.nullClass = object->isa; isClass = JKClassNull; } - else { - formatterError: - if(object == encodeCacheObject) { jk_encode_error(encodeState, @"Unable to serialize object class %@.", NSStringFromClass([encodeCacheObject class])); return(1); } - else { jk_encode_error(encodeState, @"Unable to serialize object class %@ that was returned by the unsupported class formatter. Original object class was %@.", (object == NULL) ? @"NULL" : NSStringFromClass([object class]), NSStringFromClass([encodeCacheObject class])); return(1); } - } - } - -#endif // This is here for the benefit of the optimizer. It allows the optimizer to do loop invariant code motion for the JKClassArray // and JKClassDictionary cases when printing simple, single characters via jk_encode_write(), which is actually a macro: // #define jk_encode_write1(es, dc, f) (_jk_encode_prettyPrint ? jk_encode_write1slow(es, dc, f) : jk_encode_write1fast(es, dc, f)) @@ -2618,7 +2655,7 @@ static int jk_encode_add_atom_to_buffer(JKEncodeState *encodeState, void *object default: if(JK_EXPECT_F(jk_encode_printf(encodeState, NULL, 0UL, NULL, "\\u%4.4x", utf8String[utf8Idx]))) { return(1); } break; } } else { - if(JK_EXPECT_F(utf8String[utf8Idx] == '\"') || JK_EXPECT_F(utf8String[utf8Idx] == '\\')) { encodeState->stringBuffer.bytes.ptr[encodeState->atIndex++] = '\\'; } + if(JK_EXPECT_F(utf8String[utf8Idx] == '\"') || JK_EXPECT_F(utf8String[utf8Idx] == '\\') || (JK_EXPECT_F(encodeState->serializeOptionFlags & JKSerializeOptionEscapeForwardSlashes) && JK_EXPECT_F(utf8String[utf8Idx] == '/'))) { encodeState->stringBuffer.bytes.ptr[encodeState->atIndex++] = '\\'; } encodeState->stringBuffer.bytes.ptr[encodeState->atIndex++] = utf8String[utf8Idx]; } } @@ -2672,7 +2709,7 @@ static int jk_encode_add_atom_to_buffer(JKEncodeState *encodeState, void *object else { if(JK_EXPECT_F(jk_encode_printf(encodeState, NULL, 0UL, NULL, "\\u%4.4x\\u%4.4x", (0xd7c0U + (u32ch >> 10)), (0xdc00U + (u32ch & 0x3ffU))))) { return(1); } } } } else { - if(JK_EXPECT_F(utf8String[utf8Idx] == '\"') || JK_EXPECT_F(utf8String[utf8Idx] == '\\')) { encodeState->stringBuffer.bytes.ptr[encodeState->atIndex++] = '\\'; } + if(JK_EXPECT_F(utf8String[utf8Idx] == '\"') || JK_EXPECT_F(utf8String[utf8Idx] == '\\') || (JK_EXPECT_F(encodeState->serializeOptionFlags & JKSerializeOptionEscapeForwardSlashes) && JK_EXPECT_F(utf8String[utf8Idx] == '/'))) { encodeState->stringBuffer.bytes.ptr[encodeState->atIndex++] = '\\'; } encodeState->stringBuffer.bytes.ptr[encodeState->atIndex++] = utf8String[utf8Idx]; } } @@ -2755,7 +2792,8 @@ static int jk_encode_add_atom_to_buffer(JKEncodeState *encodeState, void *object for(id keyObject in enumerateObject) { if(JK_EXPECT_T(printComma)) { if(JK_EXPECT_F(jk_encode_write1(encodeState, 0L, ","))) { return(1); } } printComma = 1; - if(JK_EXPECT_F((keyObject->isa != encodeState->fastClassLookup.stringClass)) && JK_EXPECT_F(([keyObject isKindOfClass:[NSString class]] == NO))) { jk_encode_error(encodeState, @"Key must be a string object."); return(1); } + void *keyObjectISA = *((void **)keyObject); + if(JK_EXPECT_F((keyObjectISA != encodeState->fastClassLookup.stringClass)) && JK_EXPECT_F(([keyObject isKindOfClass:[NSString class]] == NO))) { jk_encode_error(encodeState, @"Key must be a string object."); return(1); } if(JK_EXPECT_F(jk_encode_add_atom_to_buffer(encodeState, keyObject))) { return(1); } if(JK_EXPECT_F(jk_encode_write1(encodeState, 0L, ":"))) { return(1); } if(JK_EXPECT_F(jk_encode_add_atom_to_buffer(encodeState, (void *)CFDictionaryGetValue((CFDictionaryRef)object, keyObject)))) { return(1); } @@ -2766,7 +2804,8 @@ static int jk_encode_add_atom_to_buffer(JKEncodeState *encodeState, void *object for(idx = 0L; idx < dictionaryCount; idx++) { if(JK_EXPECT_T(printComma)) { if(JK_EXPECT_F(jk_encode_write1(encodeState, 0L, ","))) { return(1); } } printComma = 1; - if(JK_EXPECT_F(((id)keys[idx])->isa != encodeState->fastClassLookup.stringClass) && JK_EXPECT_F([(id)keys[idx] isKindOfClass:[NSString class]] == NO)) { jk_encode_error(encodeState, @"Key must be a string object."); return(1); } + void *keyObjectISA = *((void **)keys[idx]); + if(JK_EXPECT_F(keyObjectISA != encodeState->fastClassLookup.stringClass) && JK_EXPECT_F([(id)keys[idx] isKindOfClass:[NSString class]] == NO)) { jk_encode_error(encodeState, @"Key must be a string object."); return(1); } if(JK_EXPECT_F(jk_encode_add_atom_to_buffer(encodeState, keys[idx]))) { return(1); } if(JK_EXPECT_F(jk_encode_write1(encodeState, 0L, ":"))) { return(1); } if(JK_EXPECT_F(jk_encode_add_atom_to_buffer(encodeState, objects[idx]))) { return(1); } @@ -2804,7 +2843,7 @@ - (id)serializeObject:(id)object options:(JKSerializeOptionFlags)optionFlags enc id returnObject = NULL; if(encodeState != NULL) { [self releaseState]; } - if((encodeState = (struct JKEncodeState *)calloc(1UL, sizeof(JKEncodeState))) == NULL) { [NSException exceptionWithName:NSMallocException reason:@"Unable to allocate state structure." userInfo:NULL]; return(NULL); } + if((encodeState = (struct JKEncodeState *)calloc(1UL, sizeof(JKEncodeState))) == NULL) { [NSException raise:NSMallocException format:@"Unable to allocate state structure."]; return(NULL); } if((error != NULL) && (*error != NULL)) { *error = NULL; } diff --git a/README.md b/README.md index 395cedc..8b4a4d9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # JSONKit -JSONKit is licensed under the terms of the BSD License. Copyright © 2011, John Engelhart. +JSONKit is dual licensed under either the terms of the BSD License, or alternatively under the terms of the Apache License, Version 2.0.
+Copyright © 2011, John Engelhart. ### A Very High Performance Objective-C JSON Library - - - - - -
ParsingSerializing
Deserialize from JSONSerialize to JSON
23% Faster than Binary .plist !549% Faster than Binary .plist !
+**UPDATE:** (2011/12/18) The benchmarks below were performed before Apples [`NSJSONSerialization`][NSJSONSerialization] was available (as of Mac OS X 10.7 and iOS 5). The obvious question is: Which is faster, [`NSJSONSerialization`][NSJSONSerialization] or JSONKit? According to [this site](http://www.bonto.ch/blog/2011/12/08/json-libraries-for-ios-comparison-updated/), JSONKit is faster than [`NSJSONSerialization`][NSJSONSerialization]. Some quick "back of the envelope" calculations using the numbers reported, JSONKit appears to be approximately 25% to 40% faster than [`NSJSONSerialization`][NSJSONSerialization], which is pretty significant. + + Parsing | Serializing +:---------:|:-------------: +Deserialize from JSON | Serialize to JSON +*23% Faster than Binary* .plist* !* | *549% Faster than Binary* .plist* !* * Benchmarking was performed on a MacBook Pro with a 2.66GHz Core 2. * All JSON libraries were compiled with `gcc-4.2 -DNS_BLOCK_ASSERTIONS -O3 -arch x86_64`. @@ -21,6 +22,8 @@ JSONKit is licensed under the terms of the BSD License. Copyright © 2011, * Parsing / deserializing will automagically decompress a buffer if it detects a `gzip` signature header. * You can compress / `gzip` the serialized JSON by passing `JKSerializeOptionCompress` to `-JSONDataWithOptions:error:`. +[JSON versus PLIST, the Ultimate Showdown](http://www.cocoanetics.com/2011/03/json-versus-plist-the-ultimate-showdown/) benchmarks the common JSON libraries and compares them to Apples `.plist` format. + *** JavaScript Object Notation, or [JSON][], is a lightweight, text-based, serialization format for structured data that is used by many web-based services and API's. It is defined by [RFC 4627][]. @@ -36,15 +39,14 @@ JSON provides the following primitive types: These primitive types are mapped to the following Objective-C Foundation classes: - - - - - - - - -
JSONObjective-C
nullNSNull
true and falseNSNumber
NumberNSNumber
StringNSString
ArrayNSArray
ObjectNSDictionary
+JSON | Objective-C +-------------------|------------- +`null` | [`NSNull`][NSNull] +`true` and `false` | [`NSNumber`][NSNumber] +Number | [`NSNumber`][NSNumber] +String | [`NSString`][NSString] +Array | [`NSArray`][NSArray] +Object | [`NSDictionary`][NSDictionary] JSONKit uses Core Foundation internally, and it is assumed that Core Foundation ≡ Foundation for every equivalent base type, i.e. [`CFString`][CFString] ≡ [`NSString`][NSString]. @@ -88,16 +90,21 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S * For JSON Objects (or [`NSDictionary`][NSDictionary] in JSONKit nomenclature), [RFC 4627][] says `The names within an object SHOULD be unique` (note: `name` is a `key` in JSONKit nomenclature). At this time the JSONKit behavior is `undefined` for JSON that contains names within an object that are not unique. However, JSONKit currently tries to follow a "the last key / value pair parsed is the one chosen" policy. This behavior is not finalized and should not be depended on. The previously covered limitations regarding JSON Strings have important consequences for JSON Objects since JSON Strings are used as the `key`. The [JSON specification][RFC 4627] does not specify the details or requirements for JSON Strings used as `keys` in JSON Objects, specifically what it means for two `keys` to compare equal. Unfortunately, because [RFC 4627][] states `JSON text SHALL be encoded in Unicode.`, this means that one must use the [Unicode Standard][] to interpret the JSON, and the [Unicode Standard][] allows for strings that are encoded with different Unicode Code Points to "compare equal". JSONKit uses [`NSString`][NSString] exclusively to manage the parsed JSON Strings. Because [`NSString`][NSString] uses [Unicode][Unicode Standard] as its basis, there exists the possibility that [`NSString`][NSString] may subtly and silently convert the Unicode Code Points contained in the original JSON String in to a [Unicode equivalent][Unicode_equivalence] string. Due to this, the JSONKit behavior for JSON Strings used as `keys` in JSON Objects that may be [Unicode equivalent][Unicode_equivalence] but not binary equivalent is `undefined`. + + **See also:**
+     [W3C - Requirements for String Identity Matching and String Indexing](http://www.w3.org/TR/charreq/#sec2) ### Objective-C To JSON Primitive Mapping Details -* When serializing, the top level container, and all of its children, are required to be *strictly* [invariant][wiki_invariant] during enumeration. This property is used to make certain optimizations, such as if a particular object has already been serialized, the result of the previous serialized `UTF8` string can be reused (i.e., the `UTF8` string of the previous serialization can simply be copied instead of performing all the serialization work again). While this is probably only of interest to those who are doing extraordinarily unusual things with the run-time or custom classes inheriting from the classes that JSONKit will serialize (i.e, a custom object whose value mutates each time the it receives a message requesting its value for serialization), it also covers the case where any of the objects to be serialized are mutated during enumeration (i.e., mutated by another thread). The number of times JSONKit will request an objects value is non-deterministic, from a minimum of once up to the number of times it appears in the serialized JSON– therefore an object MUST NOT depend on receiving a message requesting its value each time it appears in the serialized output. The behavior is `undefined` if these requirements are violated. +* When serializing, the top level container, and all of its children, are required to be *strictly* [invariant][wiki_invariant] during enumeration. This property is used to make certain optimizations, such as if a particular object has already been serialized, the result of the previous serialized `UTF8` string can be reused (i.e., the `UTF8` string of the previous serialization can simply be copied instead of performing all the serialization work again). While this is probably only of interest to those who are doing extraordinarily unusual things with the run-time or custom classes inheriting from the classes that JSONKit will serialize (i.e, a custom object whose value mutates each time it receives a message requesting its value for serialization), it also covers the case where any of the objects to be serialized are mutated during enumeration (i.e., mutated by another thread). The number of times JSONKit will request an objects value is non-deterministic, from a minimum of once up to the number of times it appears in the serialized JSON– therefore an object MUST NOT depend on receiving a message requesting its value each time it appears in the serialized output. The behavior is `undefined` if these requirements are violated. * The objects to be serialized MUST be acyclic. If the objects to be serialized contain circular references the behavior is `undefined`. For example, -
[arrayOne addObject:arrayTwo];
+    ```objective-c
+    [arrayOne addObject:arrayTwo];
     [arrayTwo addObject:arrayOne];
-    id json = [arrayOne JSONString];
+ id json = [arrayOne JSONString]; + ``` … will result in `undefined` behavior. @@ -126,7 +133,9 @@ The author requests that you do not file a bug report with JSONKit regarding pro ### Important Details -* JSONKit is not designed to be used with the Mac OS X Garbage Collection. The behavior of JSONKit when compiled with `-fobj-gc` is `undefined`. It is extremely unlikely that Mac OS X Garbage Collection will ever be supported. +* JSONKit is not designed to be used with the Mac OS X Garbage Collection. The behavior of JSONKit when compiled with `-fobjc-gc` is `undefined`. It is extremely unlikely that Mac OS X Garbage Collection will ever be supported. + +* JSONKit is not designed to be used with [Objective-C Automatic Reference Counting (ARC)][ARC]. The behavior of JSONKit when compiled with `-fobjc-arc` is `undefined`. The behavior of JSONKit compiled without [ARC][] mixed with code that has been compiled with [ARC][] is normatively `undefined` since at this time no analysis has been done to understand if this configuration is safe to use. At this time, there are no plans to support [ARC][] in JSONKit. Although tenative, it is extremely unlikely that [ARC][] will ever be supported, for many of the same reasons that Mac OS X Garbage Collection is not supported. * The JSON to be parsed by JSONKit MUST be encoded as Unicode. In the unlikely event you end up with JSON that is not encoded as Unicode, you must first convert the JSON to Unicode, preferably as `UTF8`. One way to accomplish this is with the [`NSString`][NSString] methods [`-initWithBytes:length:encoding:`][NSString_initWithBytes] and [`-initWithData:encoding:`][NSString_initWithData]. @@ -148,6 +157,8 @@ The author requests that you do not file a bug report with JSONKit regarding pro * On average, the JSONData… methods are nearly four times faster than the JSONString… methods when serializing a [`NSDictionary`][NSDictionary] or [`NSArray`][NSArray] to JSON. The difference in speed is due entirely to the instantiation overhead of [`NSString`][NSString]. +* If at all possible, use [`NSData`][NSData] instead of [`NSString`][NSString] methods when processing JSON. This avoids the sometimes significant conversion overhead that [`NSString`][NSString] performs in order to provide an object oriented interface for its contents. For many uses, using [`NSString`][NSString] is not needed and results in wasted effort– for example, using JSONKit to serialize a [`NSDictionary`][NSDictionary] or [`NSArray`][NSArray] to a [`NSString`][NSString]. This [`NSString`][NSString] is then passed to a method that sends the JSON to a web server, and this invariably requires converting the [`NSString`][NSString] to [`NSData`][NSData] before it can be sent. In this case, serializing the collection object directly to [`NSData`][NSData] would avoid the unnecessary conversions to and from a [`NSString`][NSString] object. + ### Parsing Interface #### JSONDecoder Interface @@ -160,7 +171,8 @@ The objectWith… methods return immutable collection objects **Important:** `objectWithUTF8String:` and `mutableObjectWithUTF8String:` will raise [`NSInvalidArgumentException`][NSInvalidArgumentException] if `string` is `NULL`. **Important:** `objectWithData:` and `mutableObjectWithData:` will raise [`NSInvalidArgumentException`][NSInvalidArgumentException] if `jsonData` is `NULL`. -
+ (id)decoder;
+```objective-c
++ (id)decoder;
 + (id)decoderWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
 - (id)initWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
 
@@ -174,72 +186,82 @@ The objectWith… methods return immutable collection objects
 - (id)objectWithData:(NSData *)jsonData;
 - (id)objectWithData:(NSData *)jsonData error:(NSError **)error;
 - (id)mutableObjectWithData:(NSData *)jsonData;
-- (id)mutableObjectWithData:(NSData *)jsonData error:(NSError **)error;
+- (id)mutableObjectWithData:(NSData *)jsonData error:(NSError **)error; +``` #### NSString Interface -
- (id)objectFromJSONString;
+```objective-c
+- (id)objectFromJSONString;
 - (id)objectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
 - (id)objectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
 - (id)mutableObjectFromJSONString;
 - (id)mutableObjectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
-- (id)mutableObjectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
+- (id)mutableObjectFromJSONStringWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error; +``` #### NSData Interface -
- (id)objectFromJSONData;
+```objective-c
+- (id)objectFromJSONData;
 - (id)objectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
 - (id)objectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
 - (id)mutableObjectFromJSONData;
 - (id)mutableObjectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags;
-- (id)mutableObjectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error;
+- (id)mutableObjectFromJSONDataWithParseOptions:(JKParseOptionFlags)parseOptionFlags error:(NSError **)error; +``` #### JKParseOptionFlags - - - - - - + + + + + +
Parsing OptionDescription
JKParseOptionNoneThis is the default if no other other parse option flags are specified, and the option used when a convenience method does not provide an argument for explicitly specifying the parse options to use. Synonymous with JKParseOptionStrict.
JKParseOptionStrictThe JSON will be parsed in strict accordance with the RFC 4627 specification.
JKParseOptionCommentsAllow C style // and /* … */ comments in JSON. This is a fairly common extension to JSON, but JSON that contains C style comments is not strictly conforming JSON.
JKParseOptionUnicodeNewlinesAllow Unicode recommended (?:\r\n|[\n\v\f\r\x85\p{Zl}\p{Zp}]) newlines in JSON. The JSON specification only allows the newline characters \r and \n, but this option allows JSON that contains the Unicode recommended newline characters to be parsed. JSON that contains these additional newline characters is not strictly conforming JSON.
JKParseOptionLooseUnicodeNormally the decoder will stop with an error at any malformed Unicode. This option allows JSON with malformed Unicode to be parsed without reporting an error. Any malformed Unicode is replaced with \uFFFD, or REPLACEMENT CHARACTER, as specified in The Unicode 6.0 standard, Chapter 3, section 3.9 Unicode Encoding Forms.
JKParseOptionPermitTextAfterValidJSONNormally, non-white-space that follows the JSON is interpreted as a parsing failure. This option allows for any trailing non-white-space to be ignored and not cause a parsing error.
JKParseOptionNoneThis is the default if no other other parse option flags are specified, and the option used when a convenience method does not provide an argument for explicitly specifying the parse options to use. Synonymous with JKParseOptionStrict.
JKParseOptionStrictThe JSON will be parsed in strict accordance with the RFC 4627 specification.
JKParseOptionCommentsAllow C style // and /* … */ comments in JSON. This is a fairly common extension to JSON, but JSON that contains C style comments is not strictly conforming JSON.
JKParseOptionUnicodeNewlinesAllow Unicode recommended (?:\r\n|[\n\v\f\r\x85\p{Zl}\p{Zp}]) newlines in JSON. The JSON specification only allows the newline characters \r and \n, but this option allows JSON that contains the Unicode recommended newline characters to be parsed. JSON that contains these additional newline characters is not strictly conforming JSON.
JKParseOptionLooseUnicodeNormally the decoder will stop with an error at any malformed Unicode. This option allows JSON with malformed Unicode to be parsed without reporting an error. Any malformed Unicode is replaced with \uFFFD, or REPLACEMENT CHARACTER, as specified in The Unicode 6.0 standard, Chapter 3, section 3.9 Unicode Encoding Forms.
JKParseOptionPermitTextAfterValidJSONNormally, non-white-space that follows the JSON is interpreted as a parsing failure. This option allows for any trailing non-white-space to be ignored and not cause a parsing error.
### Serializing Interface The serializing interface includes [`NSString`][NSString] convenience methods for those that need to serialize a single [`NSString`][NSString]. For those that need this functionality, the [`NSString`][NSString] additions are much more convenient than having to wrap a single [`NSString`][NSString] in a [`NSArray`][NSArray], which then requires stripping the unneeded `[`…`]` characters from the serialized JSON result. When serializing a single [`NSString`][NSString], you can control whether or not the serialized JSON result is surrounded by quotation marks using the `includeQuotes:` argument: - - - - -
ExampleResultArgument
a "test"..."a \"test\"..."includeQuotes:YES
a "test"...a \"test\"...includeQuotes:NO
+Example | Result | Argument +--------------|-------------------|-------------------- +`a "test"...` | `"a \"test\"..."` | `includeQuotes:YES` +`a "test"...` | `a \"test\"...` | `includeQuotes:NO` **Note:** The [`NSString`][NSString] methods that do not include a `includeQuotes:` argument behave as if invoked with `includeQuotes:YES`. **Note:** The bytes contained in the returned [`NSData`][NSData] object are `UTF8` encoded. #### NSArray and NSDictionary Interface -
- (NSData *)JSONData;
+```objective-c
+- (NSData *)JSONData;
 - (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
 - (NSString *)JSONString;
-- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error;
+- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions error:(NSError **)error; +``` + #### NSString Interface -
- (NSData *)JSONData;
+```objective-c
+- (NSData *)JSONData;
 - (NSData *)JSONDataWithOptions:(JKSerializeOptionFlags)serializeOptions includeQuotes:(BOOL)includeQuotes error:(NSError **)error;
 - (NSString *)JSONString;
-- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions includeQuotes:(BOOL)includeQuotes error:(NSError **)error;
+- (NSString *)JSONStringWithOptions:(JKSerializeOptionFlags)serializeOptions includeQuotes:(BOOL)includeQuotes error:(NSError **)error; +``` #### JKSerializeOptionFlags - - - + + + +
Serializing OptionDescription
JKSerializeOptionNoneThis is the default if no other other serialize option flags are specified, and the option used when a convenience method does not provide an argument for explicitly specifying the serialize options to use.
JKSerializeOptionPrettyNormally the serialized JSON does not include any unnecessary white-space. While this form is the most compact, the lack of any white-space means that it's something only another JSON parser could love. Enabling this option causes JSONKit to add additional white-space that makes it easier for people to read. Other than the extra white-space, the serialized JSON is identical to the JSON that would have been produced had this option not been enabled.
JKSerializeOptionEscapeUnicodeWhen JSONKit encounters Unicode characters in NSString objects, the default behavior is to encode those Unicode characters as UTF8. This option causes JSONKit to encode those characters as \uXXXX. For example,
["w∈L⟺y(∣y∣≤∣w∣)"]
becomes:
["w\u2208L\u27fa\u2203y(\u2223y\u2223\u2264\u2223w\u2223)"]
JKSerializeOptionNoneThis is the default if no other other serialize option flags are specified, and the option used when a convenience method does not provide an argument for explicitly specifying the serialize options to use.
JKSerializeOptionPrettyNormally the serialized JSON does not include any unnecessary white-space. While this form is the most compact, the lack of any white-space means that it's something only another JSON parser could love. Enabling this option causes JSONKit to add additional white-space that makes it easier for people to read. Other than the extra white-space, the serialized JSON is identical to the JSON that would have been produced had this option not been enabled.
JKSerializeOptionEscapeUnicodeWhen JSONKit encounters Unicode characters in NSString objects, the default behavior is to encode those Unicode characters as UTF8. This option causes JSONKit to encode those characters as \uXXXX. For example,
["w∈L⟺y(∣y∣≤∣w∣)"]
becomes:
["w\u2208L\u27fa\u2203y(\u2223y\u2223\u2264\u2223w\u2223)"]
JKSerializeOptionEscapeForwardSlashesAccording to the JSON specification, the / (U+002F) character may be backslash escaped (i.e., \/), but it is not required. The default behavior of JSONKit is to not backslash escape the / character. Unfortunately, it was found some real world implementations of the ASP.NET Date Format require the date to be strictly encoded as \/Date(...)\/, and the only way to achieve this is through the use of JKSerializeOptionEscapeForwardSlashes. See github issue #21 for more information.
[JSON]: http://www.json.org/ @@ -248,6 +270,7 @@ The serializing interface includes [`NSString`][NSString] convenience methods fo [Single Precision]: http://en.wikipedia.org/wiki/Single_precision_floating-point_format [Double Precision]: http://en.wikipedia.org/wiki/Double_precision_floating-point_format [wiki_invariant]: http://en.wikipedia.org/wiki/Invariant_(computer_science) +[ARC]: http://clang.llvm.org/docs/AutomaticReferenceCounting.html [CFBoolean]: http://developer.apple.com/mac/library/documentation/CoreFoundation/Reference/CFBooleanRef/index.html [kCFBooleanTrue]: http://developer.apple.com/mac/library/documentation/CoreFoundation/Reference/CFBooleanRef/Reference/reference.html#//apple_ref/doc/c_ref/kCFBooleanTrue [kCFBooleanFalse]: http://developer.apple.com/mac/library/documentation/CoreFoundation/Reference/CFBooleanRef/Reference/reference.html#//apple_ref/doc/c_ref/kCFBooleanFalse @@ -284,3 +307,4 @@ The serializing interface includes [`NSString`][NSString] convenience methods fo [strtoull]: http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/strtoull.3.html [getrusage]: http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man2/getrusage.2.html [printf]: http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/printf.3.html +[NSJSONSerialization]: http://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html