Skip to content

maihd/cjson

Repository files navigation

cjson Build Status License: Unlicense

Simple JSON parser written in C99

Table of Contents

Features (Design Goals)

  • Simple, small and easy to use, integration.
  • Writing in C99: simple, small, portability.
  • Robust error handling, no pointer-as-object.
  • Single responsibility: when work with json in C, we only need parse the json string to C value. Json stringify is easy to implement in C.
  • No memory allocations, linear memory layout, cache friendly.
  • Well-tested with some real-world examples.
  • Visual Studio Natvis.
  • String as UTF8.

Limits

  • No scientific number
  • Do not use state machine for parsing
  • Not the best, the fastest json parser
  • Parsing require a preallocated large buffer (about kilobytes, based on file size, cannot detect buffer size requirement), buffer not an allocator (I dont often store the json value as persitent, just temp for parsing levels, game data)
  • Use longjmp to handling error, jump from error point to parse function call, which is have disadvantages:
    • longjmp may works different over platforms
    • longjmp depends on stdlib, not work on freestanding platforms
    • longjmp make call stack unpredictable, and must be unwind the call stack (which are overhead, and crash prone)

API design flaws

After sometimes use this, I found that API have flaws of its own:

  • Too verbose, not just for good reasons
  • Too many params for function call, which can be better to combine as a struct
  • Allocator buffer, but cannot custom allocations
  • It's claimed cache-friendly, but the layout of data in memory after parsing usually un-ordered, un-lineared in hierarchy point-of-view (address of items of array come before the array itself)
[Items of a array] -> [Maybe items of other array] -> [Maybe other array JSON] -> [Array JSON] -> [... and some unpredictable value, and string/object come to the party]
  • Continue to the above, reorder should help? Not exactly, how to reorder, how much reorder overhead, that too much works for simple parsing simple data format like JSON.
  • No big projects usage.
  • Parsing still too complex

Conclusion:

  • Decisioning to exploring some best practices from other parser, MetaDesk is good example. Linked list in one single arena of memory is good enough.
  • For production code, we should use battle-tested library. Mai suggestion is yyjson
  • Start new project call sjson (Simplified Json, but also mean Simplified Json Parser, more simple implementation)

Examples

Belove code from Json_TokenTest.c:

#include "Json.h"
#include "JsonUtils.h" // for JsonPrint

int main(int argc, char* argv[])
{
    signal(SIGINT, _sighandler);
    
    printf("JSON token testing prompt\n");
    printf("Type '.exit' to exit\n");
    
    char input[1024];
    int allocatorCapacity = 1024 * 1024; // 1MB temp buffer
    void* allocatorBuffer = malloc(allocatorCapacity);

    while (1)
    {
	    if (setjmp(jmpenv) == 0)
	    {
	        printf("> ");
	        fgets(input, sizeof(input), stdin);

	        const char* json = strtrim_fast(input);
	        if (strcmp(json, ".exit") == 0)
	        {
                break;
	        }
	        else
            {
                Json* value;
                JsonError error = JsonParse(json, strlen(json), JsonFlags_NoStrictTopLevel, allocatorBuffer, allocatorCapacity, &value);
                if (error.code != JsonError_None)
                {
                    value = NULL;
                    printf("[ERROR]: %s\n", error.message);
                }
                else
                {
                    JsonPrint(value, stdout); printf("\n");
                }
	        }
	    }
    }

    free(allocatorBuffer);
    return 0;
}

API

enum JsonType
{
    JsonType_Null,
    JsonType_Array,
    JsonType_Object,
    JsonType_Number,
    JsonType_String,
    JsonType_Boolean,
};

typedef enum JsonErrorCode
{
    JsonError_None,

    JsonError_WrongFormat,
    JsonError_UnmatchToken,
    JsonError_UnknownToken,
    JsonError_UnexpectedToken,
    JsonError_UnsupportedToken,

    JsonError_OutOfMemory,
    JsonError_InvalidValue,
    JsonError_InternalFatal,
} JsonErrorCode;

typedef struct JsonError
{
    JsonErrorCode   code;
    const char*     message;
} JsonError;

typedef enum JsonFlags
{
    JsonFlags_None              = 0,
    JsonFlags_SupportComment    = 1 << 0,
    JsonFlags_NoStrictTopLevel  = 1 << 1,
} JsonFlags;

struct Json
{
    JsonType                type;
    int                     length;
    union 
    {
        double              number;
        bool                boolean;   
        const char*         string;
        Json*               array;

        JsonObjectMember*   object;
    };
};

struct JsonObjectMember
{
    const char* name;
    Json        value;
};

static const Json JSON_NULL     = { JsonType_Null   , 0        };
static const Json JSON_TRUE     = { JsonType_Boolean, 0, true  };
static const Json JSON_FALSE    = { JsonType_Boolean, 0, false };

JSON_API JsonError  JsonParse(const char* jsonCode, int32_t jsonCodeLength, JsonFlags flags, void* buffer, int32_t bufferSize, Json* result);
JSON_API bool       JsonEquals(const Json a, const Json b);
JSON_API bool       JsonFind(const Json parent, const char* name, Json* result);

Build Instructions

This project is a single-header-only library, user is free to choose build system. This instructions are for development only.

# Run REPL test
make test

# Run unit tests
make unit_test

# Make single header libary (when you want to make sure Json.h/JsonEx.h is the newest version)
make lib

FAQs

Why another json parser?

When I first read an article about common mistake of c libraries are do much dynamic allocations and have no custom allocators. So I create this project to learn to make good library in C.

Have no update after long time?

Like I said above, I started this project mainly for learning purpose, how to design a good API for C. Have no purpose for compete with battle-tested library. When I have any ideas for better API design, I comeback and do some hacks. If you have ideas, just make a issue in this repo.

You said custom allocators, but I donot find one?

In the first version there is a custom allocator interface. But after the long run, I found the memory of Json was throw away at one, so dynamic allocators are expensive for that. Now we just given a temporary buffer to parser, and there is a linear allocator in internal. So no dynamic allocations. Note: Next version will combine both, support passing allocator around, but have API to create one-time allocator from buffer. (Better API)

Where stringify/serialize functions?

It easy to write an JsonStringify version, but the real problem in C is not that simple. You need to create Json value, create Json may need memory, so we need to care about memory allocation. That headache! Fortunately, C is static type language, so we can easily convert out data structure/object to json easily base on its types. See example below:

typedef struct Entity
{
    uint32_t    id;
    const char* name;
} Entity;

const char* JsonifyEntity(Entity entity)
{
    static char buffer[256];
    snprintf(buffer, sizeof(buffer), 
        "{\"id\":%u,\"name\":\"%s\"}", entity.id, entity.name
    );
    return buffer;
}

I don't like CamelCase!!!

Just rename, update, change what you do not like with your code editor.

What about licenses

This repo use UNLICENSE but the source code you generate from make can be your license of choice.

About

Simple and portable JSON parser (only) written in C99

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published