Browse files

Code comments.

Improved tests.
Updated readme.
  • Loading branch information...
1 parent a4554cb commit 2dd4253bb9eca87eaffca922d803e1fbd6be393c @kstenerud committed Nov 6, 2011
Showing with 502 additions and 129 deletions.
  1. +438 −86 KSJSON/KSJSON.m
  2. +20 −37 KSJSONTests/KSJSONTests.m
  3. +44 −6 README.md
View
524 KSJSON/KSJSON.m
@@ -29,33 +29,50 @@
#import <objc/runtime.h>
+#pragma mark Configuration
+
+/* Initial sizes when deserializing containers.
+ * Values get stored on the stack until this point, and then get transferred
+ * to the heap.
+ */
#define kDeserialize_DictionaryInitialSize 256
#define kDeserialize_ArrayInitialSize 256
+// Dictionaries of this size or less get converted on the stack instead of heap.
#define kSerialize_DictStackSize 128
+
+// Stack-based scratch buffer size (for creating certain objects).
#define kSerialize_ScratchBuffSize 1024
+
+// Starting buffer size for the serialized JSON string.
#define kSerialize_InitialBuffSize 65536
+#pragma mark Tokens
+
+// JSON parsing tokens
#define ESCAPE_BEGIN '\\'
#define STRING_BEGIN '"'
#define STRING_END '"'
#define FALSE_BEGIN 'f'
#define NULL_BEGIN 'n'
#define TRUE_BEGIN 't'
-
#define ELEMENT_SEPARATOR ','
#define NAME_SEPARATOR ':'
-
#define ARRAY_BEGIN '['
#define ARRAY_END ']'
#define DICTIONARY_BEGIN '{'
#define DICTIONARY_END '}'
+#pragma mark Macros
+
+// Compiler hints for "if" statements
#define likely_if(x) if(__builtin_expect(x,1))
#define unlikely_if(x) if(__builtin_expect(x,0))
+
+// Handles bridging and autoreleasing in ARC or non-ARC mode.
#if __has_feature(objc_arc)
#define autoreleased(X) (X)
#define cfautoreleased(X) ((__bridge_transfer id)(X))
@@ -64,24 +81,38 @@
#define cfautoreleased(X) [((__bridge_transfer id)(X)) autorelease]
#endif
+
+/** Skip whitespace. Advances CH until it's off any whitespace, or it reaches
+ * the end of the buffer.
+ *
+ * @param CH A pointer to the next character in the buffer.
+ * @param END A pointer to the end of the buffer.
+ */
#define skipWhitespace(CH,END) for(;(CH) < (END) && isspace(*(CH));(CH)++) {}
+#pragma mark Context & Helpers
+
+/** Deserializing contextual information.
+ */
typedef struct
{
- unichar** pos;
+ /** Current position in the JSON string. */
+ unichar* pos;
+ /** End of the JSON string. */
unichar* end;
+ /** Any error that has occurred. */
__autoreleasing NSError** error;
} KSJSONDeserializeContext;
-
-static CFTypeRef deserializeJSON(KSJSONDeserializeContext* context);
+// Forward reference.
static CFTypeRef deserializeElement(KSJSONDeserializeContext* context);
-static CFArrayRef deserializeArray(KSJSONDeserializeContext* context);
-static CFDictionaryRef deserializeDictionary(KSJSONDeserializeContext* context);
-static CFStringRef deserializeString(KSJSONDeserializeContext* context);
+
+/** Lookup table for converting hex values to integers.
+ * 0x77 is used to mark invalid characters.
+ */
static unichar g_hexConversion[] =
{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
@@ -92,9 +123,16 @@
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
0x77, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
-static unsigned int g_hexConversionEnd = sizeof(g_hexConversion) /
-sizeof(*g_hexConversion);
+/** Length of the conversion table. */
+static unsigned int g_hexConversionEnd = sizeof(g_hexConversion) / sizeof(*g_hexConversion);
+
+/** Make an error object with the specified message.
+ *
+ * @param error Pointer to a location to store the error object.
+ *
+ * @param fmt The message to fill the error object with.
+ */
static void makeError(NSError** error, NSString* fmt, ...)
{
if(error != nil)
@@ -110,6 +148,25 @@ static void makeError(NSError** error, NSString* fmt, ...)
}
}
+
+#pragma mark -
+#pragma mark Deserialization
+#pragma mark -
+
+
+#pragma mark String Deserialization
+
+/** Parse a unicode sequence (\u1234), placing the decoded character in dst.
+ *
+ * @param start The start of the unicode sequence.
+ *
+ * @param dst Location to store the unicode character.
+ *
+ * @param error Where to store any errors should they occur (nil = ignore).
+ *
+ * @return true if parsing was successful. If false, error will contain an
+ * explanation.
+ */
static bool parseUnicodeSequence(const unichar* start,
unichar* dst,
NSError** error)
@@ -132,24 +189,34 @@ static bool parseUnicodeSequence(const unichar* start,
return true;
}
+/** Deserialize a string.
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFStringRef deserializeString(KSJSONDeserializeContext* context)
{
- unichar* ch = *context->pos;
+ unichar* ch = context->pos;
unlikely_if(*ch != '"')
{
makeError(context->error, @"Expected a string");
return nil;
}
unlikely_if(ch[1] == '"')
{
- *context->pos = ch + 2;
+ // Empty string
+ context->pos = ch + 2;
return CFStringCreateWithCString(NULL,
"",
kCFStringEncodingUTF8);
}
+
ch++;
unichar* start = ch;
unichar* pStr = start;
+
+ // Look for a closing quote
for(;ch < context->end && *ch != '"'; ch++)
{
likely_if(*ch != '\\')
@@ -182,18 +249,20 @@ static CFStringRef deserializeString(KSJSONDeserializeContext* context)
*pStr++ = '\t';
break;
case 'u':
+ {
ch++;
unlikely_if(ch > context->end - 4)
- {
- makeError(context->error, @"Unterminated escape sequence");
- return nil;
- }
+ {
+ makeError(context->error, @"Unterminated escape sequence");
+ return nil;
+ }
unlikely_if(!parseUnicodeSequence(ch, pStr++, context->error))
- {
- return nil;
- }
+ {
+ return nil;
+ }
ch += 3;
break;
+ }
default:
makeError(context->error, @"Invalid escape sequence");
return nil;
@@ -207,14 +276,22 @@ static CFStringRef deserializeString(KSJSONDeserializeContext* context)
return nil;
}
- *context->pos = ch + 1;
+ context->pos = ch + 1;
return CFStringCreateWithCharacters(NULL,
start,
(CFIndex)(pStr - start));
}
+#pragma mark Number Deserialization
+/** Check if a character is valid for representing part of a floating point
+ * number.
+ *
+ * @param ch The character to test.
+ *
+ * @return true if the character is valid for floating point.
+ */
static bool isFPChar(unichar ch)
{
switch(ch)
@@ -228,10 +305,19 @@ static bool isFPChar(unichar ch)
}
}
+
+/** Deserialize a number.
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFNumberRef deserializeNumber(KSJSONDeserializeContext* context)
{
- unichar* start = *context->pos;
+ unichar* start = context->pos;
unichar* ch = start;
+
+ // First, try to do a simple integer conversion.
long long accum = 0;
long long sign = *ch == '-' ? -1 : 1;
if(sign == -1)
@@ -252,23 +338,33 @@ static CFNumberRef deserializeNumber(KSJSONDeserializeContext* context)
if(!isFPChar(*ch))
{
accum *= sign;
- *context->pos = ch;
+ context->pos = ch;
return CFNumberCreate(NULL, kCFNumberLongLongType, &accum);
}
+ // It's not a simple integer, so let NSDecimalNumber convert it.
for(;ch < context->end && isFPChar(*ch); ch++)
{
}
- *context->pos = ch;
+ context->pos = ch;
NSString* string = cfautoreleased(CFStringCreateWithCharacters(NULL, start, ch - start));
return (__bridge_retained CFNumberRef)[[NSDecimalNumber alloc] initWithString:string];
}
+
+#pragma mark Other Deserialization
+
+/** Deserialize "false".
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFNumberRef deserializeFalse(KSJSONDeserializeContext* context)
{
- const unichar* ch = *context->pos;
+ const unichar* ch = context->pos;
unlikely_if(context->end - ch < 5)
{
makeError(context->error, @"Premature end of JSON data");
@@ -279,14 +375,20 @@ static CFNumberRef deserializeFalse(KSJSONDeserializeContext* context)
makeError(context->error, @"Invalid characters while parsing 'false'");
return nil;
}
- *context->pos += 5;
+ context->pos += 5;
char no = 0;
return CFNumberCreate(NULL, kCFNumberCharType, &no);
}
+/** Deserialize "true".
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFNumberRef deserializeTrue(KSJSONDeserializeContext* context)
{
- const unichar* ch = *context->pos;
+ const unichar* ch = context->pos;
unlikely_if(context->end - ch < 4)
{
makeError(context->error, @"Premature end of JSON data");
@@ -297,14 +399,20 @@ static CFNumberRef deserializeTrue(KSJSONDeserializeContext* context)
makeError(context->error, @"Invalid characters while parsing 'true'");
return nil;
}
- *context->pos += 4;
+ context->pos += 4;
char yes = 1;
return CFNumberCreate(NULL, kCFNumberCharType, &yes);
}
+/** Deserialize "null".
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFNullRef deserializeNull(KSJSONDeserializeContext* context)
{
- const unichar* ch = *context->pos;
+ const unichar* ch = context->pos;
unlikely_if(context->end - ch < 4)
{
makeError(context->error, @"Premature end of JSON data");
@@ -315,46 +423,33 @@ static CFNullRef deserializeNull(KSJSONDeserializeContext* context)
makeError(context->error, @"Invalid characters while parsing 'null'");
return nil;
}
- *context->pos += 4;
+ context->pos += 4;
return kCFNull;
}
-static CFTypeRef deserializeElement(KSJSONDeserializeContext* context)
-{
- skipWhitespace(*context->pos, context->end);
-
- switch(**context->pos)
- {
- case ARRAY_BEGIN:
- return deserializeArray(context);
- case DICTIONARY_BEGIN:
- return deserializeDictionary(context);
- case STRING_BEGIN:
- return deserializeString(context);
- case FALSE_BEGIN:
- return deserializeFalse(context);
- case TRUE_BEGIN:
- return deserializeTrue(context);
- case NULL_BEGIN:
- return deserializeNull(context);
- case '0': case '1': case '2': case '3': case '4':
- case '5': case '6': case '7': case '8': case '9':
- case '-': // Begin number
- return deserializeNumber(context);
- }
- makeError(context->error, @"Unexpected character: %c", **context->pos);
- return nil;
-}
+#pragma mark Array Deserialization
+
+/** Lightweight resizable array. */
typedef struct
{
+ /** Stack storage when the array is small enough. */
CFTypeRef valuesOnStack[kDeserialize_ArrayInitialSize];
+ /** Heap storage when the array is bigger. */
CFTypeRef* values;
+ /** Length of the array buffer. */
unsigned int length;
+ /** Index of the end of the array. */
unsigned int index;
+ /** If true, we are using valuesOnStack. */
bool onStack;
} Array;
+
+/** Initialize an array.
+ *
+ * @param array The array.
+ */
static void arrayInit(Array* array)
{
array->onStack = true;
@@ -363,13 +458,24 @@ static void arrayInit(Array* array)
array->values = array->valuesOnStack;
}
+/** Add an object to an array.
+ *
+ * @param array The array to add to.
+ *
+ * @param value The object to add.
+ *
+ * @param error Holds any errors that occur (nil = ignore).
+ *
+ * @return true if the object was successfully added.
+ */
static bool arrayAddValue(Array* array, CFTypeRef value, NSError** error)
{
unlikely_if(array->index >= array->length)
{
array->length *= 2;
if(array->onStack)
{
+ // Switching from stack to heap.
array->values = malloc(array->length * sizeof(*array->values));
unlikely_if(array->values == NULL)
{
@@ -381,7 +487,8 @@ static bool arrayAddValue(Array* array, CFTypeRef value, NSError** error)
}
else
{
- array->values = realloc(array->values, array->index * sizeof(*array->values));
+ // Already on the heap, so reallocate.
+ array->values = realloc(array->values, array->length * sizeof(*array->values));
unlikely_if(array->values == NULL)
{
makeError(error, @"Out of memory");
@@ -394,6 +501,10 @@ static bool arrayAddValue(Array* array, CFTypeRef value, NSError** error)
return true;
}
+/** Free an array. All objects will be released.
+ *
+ * @param array The array to free.
+ */
static void arrayFree(Array* array)
{
for(unsigned int i = 0; i < array->index; i++)
@@ -406,20 +517,26 @@ static void arrayFree(Array* array)
}
}
+/** Deserialize an array.
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFArrayRef deserializeArray(KSJSONDeserializeContext* context)
{
- (*context->pos)++;
+ context->pos++;
Array array;
arrayInit(&array);
- while(*context->pos < context->end)
+ while(context->pos < context->end)
{
- skipWhitespace(*context->pos, context->end);
- unlikely_if(**context->pos == ARRAY_END)
+ skipWhitespace(context->pos, context->end);
+ unlikely_if(*context->pos == ARRAY_END)
{
- (*context->pos)++;
+ context->pos++;
CFTypeRef result = CFArrayCreate(NULL,
- (const void**)array.values,
+ array.values,
(CFIndex)array.index,
&kCFTypeArrayCallBacks);
arrayFree(&array);
@@ -430,10 +547,10 @@ static CFArrayRef deserializeArray(KSJSONDeserializeContext* context)
{
goto failed;
}
- skipWhitespace(*context->pos, context->end);
- likely_if(**context->pos == ELEMENT_SEPARATOR)
+ skipWhitespace(context->pos, context->end);
+ likely_if(*context->pos == ELEMENT_SEPARATOR)
{
- (*context->pos)++;
+ context->pos++;
}
arrayAddValue(&array, element, context->error);
}
@@ -444,17 +561,33 @@ static CFArrayRef deserializeArray(KSJSONDeserializeContext* context)
return nil;
}
+
+#pragma mark Dictionary Deserialization
+
+/** Lightweight resizable "dictionary data holder". */
typedef struct
{
+ /** Stack storage for keys when dictionary is small enough. */
CFTypeRef keysOnStack[kDeserialize_DictionaryInitialSize];
+ /** Stack storage for values when dictionary is small enough. */
CFTypeRef valuesOnStack[kDeserialize_DictionaryInitialSize];
+ /** Heap storage for keys when the array is bigger. */
CFTypeRef* keys;
+ /** Heap storage for values when the array is bigger. */
CFTypeRef* values;
+ /** Length of the dictionary buffer. */
unsigned int length;
+ /** Index of the end of the dictionary. */
unsigned int index;
+ /** If true, we are using keysOnStack and valuesOnStack. */
bool onStack;
} Dictionary;
+
+/** Initialize a dictionary.
+ *
+ * @param dict The dictionary to initialize.
+ */
static void dictInit(Dictionary* dict)
{
dict->onStack = true;
@@ -464,13 +597,26 @@ static void dictInit(Dictionary* dict)
dict->values = dict->valuesOnStack;
}
+/** Add a key-value pair to a dictionary.
+ *
+ * @param dict The dictionary to add to.
+ *
+ * @param key The key to add.
+ *
+ * @param value The object to add.
+ *
+ * @param error Holds any errors that occur (nil = ignore).
+ *
+ * @return true if the pair was successfully added.
+ */
static bool dictAddKeyAndValue(Dictionary* dict, CFStringRef key, CFTypeRef value, NSError** error)
{
unlikely_if(dict->index >= dict->length)
{
dict->length *= 2;
if(dict->onStack)
{
+ // Switching from stack to heap.
dict->keys = malloc(dict->length * sizeof(*dict->keys));
dict->values = malloc(dict->length * sizeof(*dict->values));
unlikely_if(dict->keys == NULL || dict->values == NULL)
@@ -484,6 +630,7 @@ static bool dictAddKeyAndValue(Dictionary* dict, CFStringRef key, CFTypeRef valu
}
else
{
+ // Already on the heap, so reallocate.
dict->keys = realloc(dict->keys, dict->length * sizeof(*dict->keys));
dict->values = realloc(dict->values, dict->length * sizeof(*dict->values));
unlikely_if(dict->keys == NULL || dict->values == NULL)
@@ -499,6 +646,10 @@ static bool dictAddKeyAndValue(Dictionary* dict, CFStringRef key, CFTypeRef valu
return true;
}
+/** Free a dictionary. All keys and objects will be released.
+ *
+ * @param dict The dictionary to free.
+ */
static void dictFree(Dictionary* dict)
{
for(unsigned int i = 0; i < dict->index; i++)
@@ -514,21 +665,27 @@ static void dictFree(Dictionary* dict)
}
}
+/** Deserialize a dictionary.
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
static CFDictionaryRef deserializeDictionary(KSJSONDeserializeContext* context)
{
- (*context->pos)++;
+ context->pos++;
Dictionary dict;
dictInit(&dict);
- while(*context->pos < context->end)
+ while(context->pos < context->end)
{
- skipWhitespace(*context->pos, context->end);
- unlikely_if(**context->pos == DICTIONARY_END)
+ skipWhitespace(context->pos, context->end);
+ unlikely_if(*context->pos == DICTIONARY_END)
{
- (*context->pos)++;
+ context->pos++;
CFTypeRef result = CFDictionaryCreate(NULL,
- (const void**)dict.keys,
- (const void**)dict.values,
+ dict.keys,
+ dict.values,
(CFIndex)dict.index,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
@@ -540,22 +697,22 @@ static CFDictionaryRef deserializeDictionary(KSJSONDeserializeContext* context)
{
goto failed;
}
- skipWhitespace(*context->pos, context->end);
- unlikely_if(**context->pos != NAME_SEPARATOR)
+ skipWhitespace(context->pos, context->end);
+ unlikely_if(*context->pos != NAME_SEPARATOR)
{
makeError(context->error, @"Expected name separator");
goto failed;
}
- (*context->pos)++;
+ context->pos++;
CFTypeRef element = deserializeElement(context);
unlikely_if(element == nil)
{
goto failed;
}
- skipWhitespace(*context->pos, context->end);
- likely_if(**context->pos == ELEMENT_SEPARATOR)
+ skipWhitespace(context->pos, context->end);
+ likely_if(*context->pos == ELEMENT_SEPARATOR)
{
- (*context->pos)++;
+ context->pos++;
}
unlikely_if(!dictAddKeyAndValue(&dict, name, element, context->error))
{
@@ -571,10 +728,51 @@ static CFDictionaryRef deserializeDictionary(KSJSONDeserializeContext* context)
}
+#pragma mark Top Level Deserialization
+
+/** Deserialize an unknown element.
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized value, or nil if an error occurred.
+ */
+static CFTypeRef deserializeElement(KSJSONDeserializeContext* context)
+{
+ skipWhitespace(context->pos, context->end);
+
+ switch(*context->pos)
+ {
+ case ARRAY_BEGIN:
+ return deserializeArray(context);
+ case DICTIONARY_BEGIN:
+ return deserializeDictionary(context);
+ case STRING_BEGIN:
+ return deserializeString(context);
+ case FALSE_BEGIN:
+ return deserializeFalse(context);
+ case TRUE_BEGIN:
+ return deserializeTrue(context);
+ case NULL_BEGIN:
+ return deserializeNull(context);
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ case '-': // Begin number
+ return deserializeNumber(context);
+ }
+ makeError(context->error, @"Unexpected character: %c", *context->pos);
+ return nil;
+}
+
+/** Begin the JSON deserialization process.
+ *
+ * @param context The deserialization context.
+ *
+ * @return The deserialized container, or nil if an error occurred.
+ */
static CFTypeRef deserializeJSON(KSJSONDeserializeContext* context)
{
- skipWhitespace(*context->pos, context->end);
- switch(**context->pos)
+ skipWhitespace(context->pos, context->end);
+ switch(*context->pos)
{
case ARRAY_BEGIN:
return deserializeArray(context);
@@ -583,31 +781,47 @@ static CFTypeRef deserializeJSON(KSJSONDeserializeContext* context)
return deserializeDictionary(context);
break;
}
- makeError(context->error, @"Unexpected character: %c", **context->pos);
+ makeError(context->error, @"Unexpected character: %c", *context->pos);
return nil;
}
+#pragma mark -
+#pragma mark Serialization
+#pragma mark -
+#pragma mark Serialization Helpers
+/** Contextual information while serializing. */
typedef struct
{
+ /** Buffer for the resulting string. */
unichar* buffer;
+ /** Scratch buffer for some operations. */
unichar scratchBuffer[kSerialize_ScratchBuffSize];
+ /** The size of the buffer. */
unsigned int size;
+ /** The current index into the buffer. */
unsigned int index;
+ /** Any error that has occurred. */
__autoreleasing NSError** error;
} KSJSONSerializeContext;
static unichar g_false[] = {'f','a','l','s','e'};
static unichar g_true[] = {'t','r','u','e'};
static unichar g_null[] = {'n','u','l','l'};
-
+// Forward declaration
static bool serializeObject(KSJSONSerializeContext* context, id object);
+/** Initialize a serialization context.
+ *
+ * @param context The context to initialize.
+ *
+ * @return true if successful.
+ */
static bool serializeInit(KSJSONSerializeContext* context)
{
context->index = 0;
@@ -623,6 +837,14 @@ static bool serializeInit(KSJSONSerializeContext* context)
return true;
}
+/** Reallocate a bigger string buffer.
+ *
+ * @param context The serialization context.
+ *
+ * @param extraCount How many more characters we ned.
+ *
+ * @return true if successful.
+ */
static bool serializeRealloc(KSJSONSerializeContext* context,
unsigned int extraCount)
{
@@ -642,6 +864,12 @@ static bool serializeRealloc(KSJSONSerializeContext* context,
return true;
}
+/** Finish the serialization process, returning the serializd JSON string.
+ *
+ * @param context The serialization context.
+ *
+ * @return The JSON string.
+ */
static NSString* serializeFinish(KSJSONSerializeContext* context)
{
return cfautoreleased(CFStringCreateWithCharactersNoCopy(NULL,
@@ -650,11 +878,21 @@ static bool serializeRealloc(KSJSONSerializeContext* context,
NULL));
}
+/** Abort the serialization process, freeing any resources.
+ *
+ * @param context The serialization context.
+ */
static void serializeAbort(KSJSONSerializeContext* context)
{
CFAllocatorDeallocate(NULL, context->buffer);
}
+/** Add 1 character to the serialized JSON string.
+ *
+ * @param context The serialization context.
+ *
+ * @param ch The character to add.
+ */
static void serializeChar(KSJSONSerializeContext* context, const unichar ch)
{
context->buffer[context->index++] = ch;
@@ -664,6 +902,14 @@ static void serializeChar(KSJSONSerializeContext* context, const unichar ch)
}
}
+/** Add 2 characters to the serialized JSON string.
+ *
+ * @param context The serialization context.
+ *
+ * @param ch1 The first character to add.
+ *
+ * @param ch2 The second character to add.
+ */
static void serialize2Chars(KSJSONSerializeContext* context,
const unichar ch1,
const unichar ch2)
@@ -676,6 +922,14 @@ static void serialize2Chars(KSJSONSerializeContext* context,
context->buffer[context->index++] = ch2;
}
+/** Add a series of characters to the serialized JSON string.
+ *
+ * @param context The serialization context.
+ *
+ * @param chars The characters to add.
+ *
+ * @param length The length of the character array.
+ */
static void serializeChars(KSJSONSerializeContext* context,
const unichar* chars,
unsigned int length)
@@ -688,12 +942,29 @@ static void serializeChars(KSJSONSerializeContext* context,
context->index += length;
}
+/** Backtrack in the serialization process, erasing previously added characters.
+ *
+ * @param context The serialization context.
+ *
+ * @param numChars The number of characters to backtrack.
+ */
static void serializeBacktrack(KSJSONSerializeContext* context,
unsigned int numChars)
{
context->index -= numChars;
}
+
+#pragma mark Object Serialization
+
+/** Serialize an array.
+ *
+ * @param context The serialization context.
+ *
+ * @param array The array to serialize.
+ *
+ * @return true if successful.
+ */
static bool serializeArray(KSJSONSerializeContext* context, NSArray* array)
{
CFArrayRef arrayRef = (__bridge CFArrayRef)array;
@@ -720,6 +991,14 @@ static bool serializeArray(KSJSONSerializeContext* context, NSArray* array)
}
+/** Serialize a dictionary.
+ *
+ * @param context The serialization context.
+ *
+ * @param dict The dictionary to serialize.
+ *
+ * @return true if successful.
+ */
static bool serializeDictionary(KSJSONSerializeContext* context,
NSDictionary* dict)
{
@@ -733,8 +1012,11 @@ static bool serializeDictionary(KSJSONSerializeContext* context,
return true;
}
serializeChar(context, '{');
+
const void** keys;
const void** values;
+
+ // Try to use the stack, otherwise fall back on the heap.
void* memory = NULL;
const void* stackMemory[kSerialize_DictStackSize * 2];
likely_if(count <= kSerialize_DictStackSize)
@@ -783,6 +1065,14 @@ static bool serializeDictionary(KSJSONSerializeContext* context,
return success;
}
+/** Serialize a string.
+ *
+ * @param context The serialization context.
+ *
+ * @param string The string to serialize.
+ *
+ * @return true if successful.
+ */
static bool serializeString(KSJSONSerializeContext* context, NSString* string)
{
void* memory = NULL;
@@ -874,6 +1164,12 @@ static bool serializeString(KSJSONSerializeContext* context, NSString* string)
return true;
}
+/** Serialize a number represented as a string of digits.
+ *
+ * @param context The serialization context.
+ *
+ * @param numberString The number to serialize.
+ */
static void serializeNumberString(KSJSONSerializeContext* context,
const char* numberString)
{
@@ -884,6 +1180,12 @@ static void serializeNumberString(KSJSONSerializeContext* context,
}
}
+/** Serialize an integer.
+ *
+ * @param context The serialization context.
+ *
+ * @param value The value to serialize.
+ */
static void serializeInteger(KSJSONSerializeContext* context,
long long value)
{
@@ -910,6 +1212,14 @@ static void serializeInteger(KSJSONSerializeContext* context,
serializeChars(context, ptr, (unsigned int)(buff + 30 - ptr));
}
+/** Serialize a number.
+ *
+ * @param context The serialization context.
+ *
+ * @param number The number to serialize.
+ *
+ * @return true if successful.
+ */
static bool serializeNumber(KSJSONSerializeContext* context,
NSNumber* number)
{
@@ -1047,15 +1357,24 @@ static bool serializeNumber(KSJSONSerializeContext* context,
return nil;
}
+/** Serialize a null value.
+ *
+ * @param context The serialization context.
+ *
+ * @param object The object to serialize (unused).
+ *
+ * @return always true.
+ */
static bool serializeNull(KSJSONSerializeContext* context, id object)
{
serializeChars(context, g_null, 4);
return true;
}
-
+// Prototype for a serialization routine.
typedef bool (*serializeFunction)(KSJSONSerializeContext* context, id object);
+/** The different kinds of classes we are interested in. */
typedef enum
{
KSJSON_ClassString,
@@ -1066,7 +1385,10 @@ static bool serializeNull(KSJSONSerializeContext* context, id object)
KSJSON_ClassCount,
} KSJSON_Class;
+/** Cache for holding classes we've seen before. */
static Class g_classCache[KSJSON_ClassCount];
+
+/** Pointers to functions that can serialize various classes of object. */
static const serializeFunction g_serializeFunctions[] =
{
serializeString,
@@ -1076,8 +1398,17 @@ static bool serializeNull(KSJSONSerializeContext* context, id object)
serializeNull,
};
+/** Serialize an object.
+ *
+ * @param context The serialization context.
+ *
+ * @param object The object to serialize.
+ *
+ * @return true if successful.
+ */
static bool serializeObject(KSJSONSerializeContext* context, id object)
{
+ // Check the cache first.
Class cls = object_getClass(object);
for(KSJSON_Class i = 0; i < KSJSON_ClassCount; i++)
{
@@ -1087,6 +1418,7 @@ static bool serializeObject(KSJSONSerializeContext* context, id object)
}
}
+ // Failing that, look it up the long way and cache the class.
KSJSON_Class classType = KSJSON_ClassCount;
if([object isKindOfClass:[NSString class]])
{
@@ -1119,8 +1451,21 @@ static bool serializeObject(KSJSONSerializeContext* context, id object)
return g_serializeFunctions[classType](context, object);
}
+
+#pragma mark -
+#pragma mark Objective-C Implementation
+#pragma mark -
+
@implementation KSJSON
+/** Serialize an object.
+ *
+ * @param object The object to serialize.
+ *
+ * @param error Place to store any error that occurs (nil = ignore).
+ *
+ * @return The serialized object or nil if an error occurred.
+ */
+ (NSString*) serializeObject:(id) object error:(NSError**) error
{
if(error != nil)
@@ -1146,6 +1491,14 @@ + (NSString*) serializeObject:(id) object error:(NSError**) error
return nil;
}
+/** Deserialize a JSON string.
+ *
+ * @param jsonString The string to deserialize.
+ *
+ * @param error Place to store any error that occurs (nil = ignore).
+ *
+ * @return The deserialized object or nil if an error occurred.
+ */
+ (id) deserializeString:(NSString*) jsonString error:(NSError**) error
{
if(error != nil)
@@ -1161,11 +1514,10 @@ + (id) deserializeString:(NSString*) jsonString error:(NSError**) error
return nil;
}
CFStringGetCharacters(stringRef, CFRangeMake(0, length), (UniChar*)start);
- unichar* chars = start;
unichar* end = start + length;
KSJSONDeserializeContext context =
{
- &chars,
+ start,
end,
error
};
@@ -1174,7 +1526,7 @@ + (id) deserializeString:(NSString*) jsonString error:(NSError**) error
unlikely_if(error != nil && *error != nil)
{
NSString* desc = [(*error).userInfo valueForKey:NSLocalizedDescriptionKey];
- desc = [desc stringByAppendingFormat:@" (at offset %d)", chars - start];
+ desc = [desc stringByAppendingFormat:@" (at offset %d)", context.pos - start];
*error = [NSError errorWithDomain:@"KSJSON"
code:1
userInfo:[NSDictionary dictionaryWithObject:desc
View
57 KSJSONTests/KSJSONTests.m
@@ -785,32 +785,16 @@ - (void) testSerializeDeserializeEmptyString
STAssertEqualObjects(result, original, @"");
}
-
- (void) testSerializeDeserializeBigString
{
NSError* error = (NSError*)self;
- NSString* string =
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789"
- @"01234567890123456789012345678901234567890123456789";
+
+ unsigned int length = 500;
+ NSMutableString* string = [NSMutableString stringWithCapacity:length];
+ for(unsigned int i = 0; i < length; i++)
+ {
+ [string appendFormat:@"%d", i%10];
+ }
NSString* expected = [NSString stringWithFormat:@"[\"%@\"]", string];
id original = [NSArray arrayWithObjects:
@@ -849,22 +833,21 @@ - (void) testSerializeDeserializeHugeString
- (void) testSerializeDeserializeLargeArray
{
NSError* error = (NSError*)self;
- NSString* jsonString = @"["
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9,"
- "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9," "0,1,2,3,4,5,6,7,8,9"
- "]";
+ unsigned int numEntries = 2000;
+
+ NSMutableString* jsonString = [NSMutableString string];
+ [jsonString appendString:@"["];
+ for(unsigned int i = 0; i < numEntries; i++)
+ {
+ [jsonString appendFormat:@"%d,", i%10];
+ }
+ [jsonString deleteCharactersInRange:NSMakeRange([jsonString length]-1, 1)];
+ [jsonString appendString:@"]"];
+
id deserialized = [KSJSON deserializeString:jsonString error:&error];
STAssertNotNil(deserialized, @"");
STAssertNil(error, @"");
- STAssertEquals([deserialized count], 300u, @"");
+ STAssertEquals([deserialized count], numEntries, @"");
NSString* serialized = [KSJSON serializeObject:deserialized error:&error];
STAssertNotNil(serialized, @"");
STAssertNil(error, @"");
@@ -878,7 +861,7 @@ - (void) testSerializeDeserializeLargeArray
- (void) testSerializeDeserializeLargeDictionary
{
NSError* error = (NSError*)self;
- unsigned int numEntries = 500;
+ unsigned int numEntries = 2000;
NSMutableString* jsonString = [NSMutableString string];
[jsonString appendString:@"{"];
View
50 README.md
@@ -1,17 +1,55 @@
KSJSON
======
-Fast JSON serialization and deserialization in Objective-C.
+The fastest JSON parser and serializer for Objective-C.
-KSJSON is currently the fastest JSON serializer, and the second fastest
-deserializer, as tested in https://github.com/kstenerud/JSONCompare
-KSJSON supports ARC.
+
+Why KSJSON?
+-----------
+
+### It's Fast!
+
+KSJSON is simply the fastest Objective-C based JSON converter there is.
+Benchmarks are available at https://github.com/kstenerud/JSONCompare
+
+
+### It's Simple!
+
+There are two files to add to your project: *KSJSON.h* and *KSJSON.m*.
+
+There are two API calls: *serializeObject* and *deserializeString*. That's it.
+
+No extra dependencies. No special include paths. No extra linker options.
+No pollution of NSArray and friends with helper categories that might clash
+with another library.
+
+
+### It's Reliable!
+
+KSJSON includes 70 unit tests, which have 90% code coverage.
+
+
+### It's Small!
+
+The armv7 object code for KSJSON weighs in at 42 kilobytes.
+
+
+### It supports ARC!
+
+KSJSON supports compiling with or without ARC.
+
+
+
+Installation
+------------
+
+Copy *KSJSON.h* and *KSJSON.m* into your project.
Usage
-=====
+-----
### Serialize:
@@ -28,7 +66,7 @@ Usage
License
-=======
+-------
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

0 comments on commit 2dd4253

Please sign in to comment.