Embed JSON in Embedded? Easy peasy

    image

    Not so long ago, I had a need to load the application configuration with very limited resources. There was practically no access to any of the standard C functions. It was very lucky that there were standard functions for working with memory malloc () / free () .

    The following situation developed: the configuration is read from the file when the application is loaded on a system with limited resources. The configuration itself should be easily edited on a regular computer, up to the point that it will be necessary to quickly correct several values ​​directly on the object when demonstrating to the customer.

    From this we can conclude that we must either:
    1. Write your own binary format editor.
    2. Use text format.


    The first option does not suit us due to its limitations and scalability. If we add several new fields to the structure of the binary file, we need to recompile (at best) the file editor. Therefore, we will use the second option.

    But I did not want to fence my text format, especially since everything was invented before us. We are not the first to have a similar problem. Therefore, we take one of the more or less standard text formats for storing data.

    There are such a choice:

    • XML
    • Json
    • Yaml
    • Ini
    • Lua files (something more advanced than just config)
    • Files of the form `key = value`

    I’ll say right away that XML was dropped right away because it was too redundant for our task. YAML is an interesting format, but is still widespread in fairly narrow circles. Files of type `key = value` are too simple. It is quite difficult to store any complex values ​​in them. Try to save the exact time with the date in this format, here or write your own key for each value (year = ..., month = ..., ..., second = ...), which is just a parse, but it looks awful, either write your date parser to the value of `date = 2005-08-09T18: 31: 42`. And if you need to save the entity representing the employee, in which there are fifty fields, right? For the same reason, INI files are dropped. Lua-configs are very interesting and integrate quite simply, adding a sea of ​​opportunities. But, again, I did not want to start a virtual machine for the sake of parsing a text file.

    Therefore, in the end, the choice fell on JSON. Common enough. Scalable enough. Easy to edit and learn (in case it will have to be edited by a semi-IT specialist).

    So, we decided on the format, it remains to find a parser of this format absolutely without any dependencies ... so, stop. The only thing that was found with such a “filter” was the jsmn parser . But the problem is that this is the parser. It does not form json objects, but only parses the string into tokens. And if the format of the downloaded * .json file is known in advance, then you can simply get all the values. But stop, if the format of the downloaded file is known in advance, then why not use the binary format? Therefore, imagine that we do not know the file format. Therefore, we will quickly write our wrapper over jsmn.

    And so the Json For Embedded Systems (JFES) project was born .

    Key features


    • Compatible with C99
    • Absolutely no dependencies.
    • Easy to port.
    • It can only be used as a parser.

    The JFES library is based on two files: jfes.h and jfes.c, and the object around which everything revolves is jfes_value_t.

    /** JSON value structure. */
    struct jfes_value {
        jfes_value_type_t       type;               /**< JSON value type. */
        jfes_value_data_t       data;               /**< Value data. */
    };
    

    In turn, the type field can take values:

    /** JFES token types */
    typedef enum jfes_token_type {
        jfes_undefined          = 0x00,             /**< Undefined token type. */
        jfes_null               = 0x01,             /**< Null token type. */
        jfes_boolean            = 0x02,             /**< Boolean token type. */
        jfes_integer            = 0x03,             /**< Integer token type. */
        jfes_double             = 0x04,             /**< Double token type. */
        jfes_string             = 0x05,             /**< String token type. */
        jfes_array              = 0x06,             /**< Array token type. */
        jfes_object             = 0x07,             /**< Object token type. */
    } jfes_token_type_t;
    /** Json value type is the same as token type. */
    typedef jfes_token_type_t jfes_value_type_t;
    

    And the data field is union:

    /** JFES value data union. */
    typedef union jfes_value_data {
        int                     bool_val;           /**< Boolean JSON value. */
        int                     int_val;            /**< Integer JSON value. */
        double                  double_val;         /**< Double JSON value. */
        jfes_string_t           string_val;         /**< String JSON value. */
        jfes_array_t            *array_val;         /**< Array JSON value. */
        jfes_object_t           *object_val;        /**< Object JSON value. */
    } jfes_value_data_t;
    

    Initialization


    To initialize the library, you must initialize the jfes_config_t object.

    /** JFES config structure. */
    typedef struct jfes_config {
        jfes_malloc_t           jfes_malloc;        /**< Memory allocation function. */
        jfes_free_t             jfes_free;          /**< Memory deallocation function. */
    } jfes_config_t;
    

    Remember I said that JFES is completely without dependencies? That's right, she herself does not even know how to allocate memory, and in her sources you will not find a single #include. You can specify your memory allocation functions if you want, for debugging purposes, check the allocated memory, or because you only have your memory management functions.

    jfes_config_t config;
    config.jfes_malloc = malloc;
    config.jfes_free = free;
    

    All! After that, you can parse the json string as you like. You can edit any value, and after that save again to a line. A small example of working with JFES.

    We will parse such JSON:

    {
        "first_name": "John",
        "last_name": "Black",
        "age": 35,
        "children": [
            { "first_name": "Alice", "age": 5 },
            { "first_name": "Robert", "age": 8 },
        ],
        "wife": null,
        "simple" : [ 12, 15, 76, 34, 75, "Test", 23.1, 65.3, false, true, false ]
    }
    

    We will parse it using this code:

    jfes_config_t config;
    config.jfes_malloc = malloc;
    config.jfes_free = free;
    jfes_value_t value;
    jfes_parse_to_value(&config, json_data, json_size, &value);
    /* Получаем указатель на массив children. Если такого ключа нет, то children будет JFES_NULL. */
    jfes_value_t *children = jfes_get_child(&value, "children", 0);
    /* А теперь мы хотим добавить в этот массив нового ребёнка. 
      Для этого:
       1. Создаём объект.
       2. Устанавливаем ему свойства.
       3. Помещаем его в массив.
    */
    jfes_value_t *child = jfes_create_object_value(&config);
    /* 
       Ниже мы создаём строку "Paul" и помещаем её под ключом "first_name".
    */
    jfes_set_object_property(&config, child, 
       jfes_create_string_value(&config, "Paul", 0), 
       "first_name", 0);
    /* То же самое делаем с "middle_name" и "age". */
    jfes_set_object_property(&config, child, 
       jfes_create_string_value(&config, "Smith", 0), 
       "middle_name", 0);
    jfes_set_object_property(&config, child, 
       jfes_create_integer_value(&config, 1), 
       "age", 0);
    /* Перезаписываем возраст на 2. Важный момент: 
       если объект с таким ключом уже существует,
       то мы перезаписываем его. 
       Если его не существует, то он создаётся. */
    jfes_set_object_property(&config, child, 
       jfes_create_integer_value(&config, 2), "age", 0);
    /* Убираем из объекта child свойство "middle_name" */
    jfes_remove_object_property(&config, child, "middle_name", 0);
    /* Помещаем объект `child` в массив `children` по индексу 1. */
    status = jfes_place_to_array_at(&config, children, child, 1);
    jfes_set_object_property(&config, &value, 
       jfes_create_null_value(&config), "null_property", 0);
    /* 
       А теперь сериализуем полученный объект в строку.
       В dump_size, в итоге, будет лежать размер полученной строки. 
       Она НЕ будет нуль-терминированной.
       Если последним параметром передать не 1, а 0, то вывод в строку будет 
       одной строкой, без переносов, пробелов и т.п. (ugly).
    */
    jfes_size_t dump_size = 1024;
    char *beauty_dump = malloc(dump_size * sizeof(char));
    jfes_value_to_string(&value, &beauty_dump[0], &dump_size, 1);
    beauty_dump[dump_size] = '\0';
    free(beauty_dump);
     /* Обязательно надо освобождать каждое значение во избежание утечек памяти. */
     jfes_free_value(&config, &value);
    

    Afterword


    The library is not perfect, like any other code. It has errors, omissions, typos. But this is the power of Open Source. I will be glad to any pull-requests, comments, tickets and wishes.

    Link to my github: JFES

    I hope that at least someone will find it useful and save a week of bike building . As far as my strength and capabilities, I will modify it. All nightly commits will be in the experimental branch, in the master a more or less stable and tested code will merge. The exception will be only for the alpha stage - while the product is in alpha, the master branch may break, but I will try not to break anything and still use experimental as much as possible.

    Prior to version 1.0.0, the API may change, be careful. But API changes will be written in the description of commit`a.

    Many thanks to zserge for the jsmn library .

    Good luck!

    Also popular now: