我已经在C语言中创建了一个JSON解析库,并希望得到一些反馈(可以在GitHub上提交拉取请求)。任何和所有建议都是可以接受的,但是如果审核集中在提高效率上,我希望这样做。

我的头文件:

#ifndef _json_H_
#define _json_H_

/**
 * JSON type identifier. Basic types are:
 *  o Object
 *  o Array
 *  o String
 *  o Other primitive: number, boolean (true/false) or null
 */
typedef enum { JSON_PRIMITIVE, JSON_OBJECT, JSON_ARRAY, JSON_STRING } JsonType;

typedef enum
{
    JSON_ERROR_NOMEM = -1, // Not enough tokens were provided
    JSON_ERROR_INVAL = -2, // Invalid character inside JSON string
    JSON_ERROR_PART = -3, // The string is not a full JSON packet, more bytes expected
    JSON_SUCCESS = 0 // Everthing is fine
} JsonError;

/**
 * JSON token description.
 * @param       type    type (object, array, string etc.)
 * @param       start   start position in JSON data string
 * @param       end     end position in JSON data string
 */
typedef struct
{
    JsonType type;
    int start;
    int end;
    int size;
    #ifdef json_PARENT_LINKS
    int parent;
    #endif
} JsonToken;

/**
 * JSON parser. Contains an array of token blocks available. Also stores
 * the string being parsed now and current position in that string
 */
typedef struct
{
    unsigned int pos; /* offset in the JSON string */
    unsigned int toknext; /* next token to allocate */
    int toksuper; /* superior token node, e.g parent object or array */
} JsonParser;

/**
 * Create JSON parser over an array of tokens
 */
void json_initJsonParser(JsonParser *parser);

/**
 * Run JSON parser. It parses a JSON data string into and array of tokens, each describing
 * a single JSON object.
 */
JsonError json_parseJson(JsonParser *parser, const char *js, JsonToken *tokens, unsigned int tokenNum);

#endif /* _json_H_ */


我的C源代码:

/** 
 * @file json.c
 * @brief JSON Parser
 */

#include <stdlib.h>
#include "json.h"

/**
 * @fn static JsonToken *json_allocJsonToken(JsonParser *parser, JsonToken *tokens, size_t tokenNum)
 * @brief Allocates a fresh unused token from the token pull.
 * @param parser
 * @param tokens
 * @param tokenNum
 */
static JsonToken *json_allocJsonToken(JsonParser *parser, JsonToken *tokens, size_t tokenNum)
{
    if (parser->toknext >= tokenNum) return NULL;
    JsonToken *tok = &tokens[parser->toknext++];
    tok->start = tok->end = -1;
    tok->size = 0;
    #ifdef json_PARENT_LINKS
    tok->parent = -1;
    #endif
    return tok;
}

/**
 * @fn static void json_fillToken(JsonToken *token, JsonType type, int start, int end)
 * @brief Fills token type and boundaries.
 * @param token
 * @param type
 * @param start
 * @param end
 */
static void json_fillToken(JsonToken *token, JsonType type, int start, int end)
{
    token->type = type;
    token->start = start;
    token->end = end;
    token->size = 0;
}

/**
 * @fn static JsonError json_parsePrimitive(JsonParser *parser, const char *js, JsonToken *tokens, size_t num_tokens)
 * @brief Fills next available token with JSON primitive.
 * @param parser
 * @param js
 * @param tokens
 * @param num_tokens
 */
static JsonError json_parsePrimitive(JsonParser *parser, const char *js, JsonToken *tokens, size_t num_tokens)
{
    JsonToken *token;
    int start;

    start = parser->pos;

    for (; js[parser->pos] != 'q4312078q'; parser->pos++)
    {
        switch (js[parser->pos]) 
        {
            #ifndef json_STRICT
            /* In strict mode primitive must be followed by "," or "}" or "]" */
            case ':':
            #endif
            case '\t': 
            case '\r': 
            case '\n': 
            case ' ':
            case ',': 
            case ']': 
            case '}':
                goto found;
        }
        if (js[parser->pos] < 32 || js[parser->pos] >= 127)
        {
            parser->pos = start;
            return JSON_ERROR_INVAL;
        }
    }
    #ifdef json_STRICT
    /* In strict mode primitive must be followed by a comma/object/array */
    parser->pos = start;
    return JSON_ERROR_PART;
    #endif

    found:
    token = json_allocJsonToken(parser, tokens, num_tokens);
    if (!token)
    {
        parser->pos = start;
        return JSON_ERROR_NOMEM;
    }
    json_fillToken(token, JSON_PRIMITIVE, start, parser->pos);
    #ifdef json_PARENT_LINKS
    token->parent = parser->toksuper;
    #endif
    parser->pos--;
    return JSON_SUCCESS;
}

/**
 * @fn static JsonError json_parseString(JsonParser *parser, const char *js, JsonToken *tokens, size_t num_tokens)
 * @brief Fills next token with JSON string.
 * @param parser
 * @param js 
 * @param tokens
 * @param num_tokens
 */
static JsonError json_parseString(JsonParser *parser, const char *js, JsonToken *tokens, size_t num_tokens)
{
    JsonToken *token;
    int start = parser->pos;

    parser->pos++;

    /* Skip starting quote */
    for (; js[parser->pos] != 'q4312078q'; parser->pos++)
    {
        char c = js[parser->pos];

        /* Quote: end of string */
        if (c == '\"')
        {
            token = json_allocJsonToken(parser, tokens, num_tokens);
            if (!token)
            {
                parser->pos = start;
                return JSON_ERROR_NOMEM;
            }
            json_fillToken(token, JSON_STRING, start+1, parser->pos);
            #ifdef json_PARENT_LINKS
            token->parent = parser->toksuper;
            #endif
            return JSON_SUCCESS;
        }

        /* Backslash: Quoted symbol expected */
        if (c == '\')
        {
            parser->pos++;
            switch (js[parser->pos])
            {
                /* Allowed escaped symbols */
                case '\"': 
                case '/': 
                case '\': 
                case 'b':
                case 'f': 
                case 'r': 
                case 'n': 
                case 't':
                    break;
                /* Allows escaped symbol \uXXXX */
                case 'u':
                    /// \todo handle JSON unescaped symbol \uXXXX
                    break;
                /* Unexpected symbol */
                default:
                    parser->pos = start;
                    return JSON_ERROR_INVAL;
            }
        }
    }
    parser->pos = start;
    return JSON_ERROR_PART;
}

/**
 * @fn JsonError json_parseJson(JsonParser *parser, const char *js, JsonToken *tokens, unsigned int num_tokens) 
 * @brief Parse JSON string and fill tokens.
 * @param parser
 * @param js
 * @param tokens
 * @param num_tokens
 */
JsonError json_parseJson(JsonParser *parser, const char *js, JsonToken *tokens, unsigned int num_tokens) 
{
    JsonError r;
    int i;
    JsonToken *token;

    for (; js[parser->pos] != 'q4312078q'; parser->pos++)
    {
        char c;
        JsonType type;

        c = js[parser->pos];
        switch (c)
        {
            case '{':
            case '[':
                token = json_allocJsonToken(parser, tokens, num_tokens);
                if (!token) return JSON_ERROR_NOMEM;
                if (parser->toksuper != -1)
                {
                    tokens[parser->toksuper].size++;
                    #ifdef json_PARENT_LINKS
                    token->parent = parser->toksuper;
                    #endif
                }
                token->type = (c == '{' ? JSON_OBJECT : JSON_ARRAY);
                token->start = parser->pos;
                parser->toksuper = parser->toknext - 1;
                break;
            case '}':
            case ']':
                type = (c == '}' ? JSON_OBJECT : JSON_ARRAY);
                #ifdef json_PARENT_LINKS
                if (parser->toknext < 1) return JSON_ERROR_INVAL;
                token = &tokens[parser->toknext - 1];
                for (;;)
                {
                    if (token->start != -1 && token->end == -1)
                    {
                        if (token->type != type) return JSON_ERROR_INVAL;
                        token->end = parser->pos + 1;
                        parser->toksuper = token->parent;
                        break;
                    }
                    if (token->parent == -1) break;
                    token = &tokens[token->parent];
                }
                #else
                for (i = parser->toknext - 1; i >= 0; i--)
                {
                    token = &tokens[i];
                    if (token->start != -1 && token->end == -1)
                    {
                        if (token->type != type) return JSON_ERROR_INVAL;
                        parser->toksuper = -1;
                        token->end = parser->pos + 1;
                        break;
                    }
                }
                /* Error if unmatched closing bracket */
                if (i == -1) return JSON_ERROR_INVAL;
                for (; i >= 0; i--)
                {
                    token = &tokens[i];
                    if (token->start != -1 && token->end == -1)
                    {
                        parser->toksuper = i;
                        break;
                    }
                }
                #endif
                break;
            case '\"':
                r = json_parseString(parser, js, tokens, num_tokens);
                if (r < 0) return r;
                if (parser->toksuper != -1) tokens[parser->toksuper].size++;
                break;
            case '\t':
            case '\r':
            case '\n':
            case ':':
            case ',':
            case ' ': 
                break;
            #ifdef json_STRICT
            /* In strict mode primitives are: numbers and booleans */
            case '-':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case 't':
            case 'f':
            case 'n':
            #else
            /* In non-strict mode every unquoted value is a primitive */
            default:
            #endif
                r = json_parsePrimitive(parser, js, tokens, num_tokens);
                if (r < 0) return r;
                if (parser->toksuper != -1) tokens[parser->toksuper].size++;
                break;

            #ifdef json_STRICT
            /* Unexpected char in strict mode */
            default:
                return JSON_ERROR_INVAL;
            #endif

        }
    }

    for (i = parser->toknext - 1; i >= 0; i--)
    {
        /* Unmatched opened object or array */
        if (tokens[i].start != -1 && tokens[i].end == -1) return JSON_ERROR_PART;
    }

    return JSON_SUCCESS;
}

/**
 * @fn void json_initJsonParser(JsonParser *parser)
 * @brief Creates a new parser based over a given buffer with an array of tokens available.
 * @param parser
 */
void json_initJsonParser(JsonParser *parser)
{
    parser->pos = 0;
    parser->toknext = 0;
    parser->toksuper = -1;
}


#1 楼

您做得很好


据我所知,只要输入正确的代码,代码就会产生正确的输出。
函数感觉就像一个连贯的库,具有一致的函数名和参数的逻辑顺序。
您使用了调用方拥有的所有内存管理策略,该策略对C效果很好。

您可以改进的地方


名称json_initJsonParser()有点多余。 json_initParser()应该足够了。
我认为#ifdef json_PARENT_LINKS不是一个好主意。用户可以在不支持父链接的情况下轻松地编译库,但是使用已定义的json_PARENT_LINKS编译应用程序,从而导致讨厌的错误。为什么不总是启用支持父链接?如果没有解析器输出,将很难理解。
我认为#ifdef json_STRICT作为运行时选项而不是编译时选项会更好。如果用户想同时使用两种模式,则没有合理的方法。
JSON原语可能是数字truefalsenull。您接受诸如truthy之类的非法基元。

json_allocJsonToken()不会按照我期望的方式“分配”内存。同样,在对json_allocJsonToken()的三个调用中,有两个后跟json_fillToken(),因此您最好将这两个函数结合起来。首先填写伪值是没有意义的。
JsonParser结构中,toknexttoksuper应该是相同的数据类型。奇怪的是,一个未签名,而另一个已签名。我建议仅对两者都使用int,或者对ssize_t都使用。
请随时在头文件中添加一些带有用法示例的慷慨注释。



评论


\ $ \ begingroup \ $
我认为使用ssize_t并不是一个好主意,因为那样会使库与POSIX绑定。该范围也不是直观的:它必须能够保持的唯一负值是-1(请在链接的问题中查看此答案)。
\ $ \ endgroup \ $
–杰瓦
15年7月16日在18:18

#2 楼

我认为代码看起来很整洁。不确定是否可以大大提高速度-您通过存储令牌的元数据(开始/结束位置和类型)有效地对字符串进行了令牌化。这减轻了必须复制数据并处理相关的内存分配问题的痛苦。

一些注意事项:


您可以考虑将令牌更改为而是存储一个const char * start和一个size_t length,其中start指向输入中令牌的第一个字符。这样,您就可以处理令牌的数据,而不必传递对原始字符串的引用。
您有方法的文档,但实际上并没有文档化参数-它们的用途,作用
对于一般用途,该接口可能太不方便-您几乎需要在解析(或执行旧的parse - fail - double memory - try again舞蹈)之前知道期望有多少个令牌,这可能很难跟踪。作为用户,我可能知道我想解析哪种对象,但很难准确地预先知道我需要多少个json标记。
goto通常令人讨厌。尽管您以一种非常简单明了的方式轻松使用它,但是我几乎可以肯定,使用gotValidToken令牌标志重写代码以终止循环可能会提高可读性。 goto迫使您在阅读代码时跳来跳去。