Встроить JSON в Embedded? Проще простого

image

Не так давно у меня появилась необходимость загружать конфигурацию приложения при очень ограниченных ресурсах. Не было доступа, практически, ни к каким стандартным функциям C. Очень повезло, что были стандартные функции по работе с памятью malloc()/free().

Сложилась следующая ситуация: конфигурация считывается из файла при загрузке приложения на системе с ограниченными ресурсами. Сама же конфигурация должна легко редактироваться на обычном компьютере вплоть до того, что необходимо будет поправить быстро несколько значения прямо на объекте при демонстрации заказчику.

Из этого можно сделать вывод, что надо либо:

  1. Писать свой редактор бинарного формата.
  2. Использовать текстовый формат.


Первый вариант нам не подходит ввиду своей ограниченности и проблемной масштабируемости. Если мы добавляем несколько новых полей в структуру бинарного файла, нам необходимо перекомпилировать (в лучшем случае) и редактор файлов. Поэтому мы будем использовать второй вариант.

Но городить свой текстовый формат не хотелось, тем более, что всё придумано до нас. Мы не первые, у кого возникла подобная проблема. Поэтому возьмём один из более-менее стандартных текстовых форматов для хранения данных.

На выбор есть такие:

  • XML
  • JSON
  • YAML
  • INI
  • Lua файлы (что-то более продвинутое, чем просто конфиг)
  • Файлы вида `key=value`


Скажу сразу, что XML отпал сразу, потому что для нашей задачи он был слишком избыточен. YAML интересный формат, но распространён пока ещё в достаточно узких кругах. Файлы типа `key=value` слишком просты. В них достаточно тяжело хранить какие-либо сложные значения. Попробуйте сохранить в таком формате точное время с датой, тут или писать на каждое значение свой ключ (year=..., month=..., ..., second=...), что просто парсить, но выглядит ужасно, либо писать свой парсер даты на значение `date=2005-08-09T18:31:42`. А если надо сохранить сущность, представляющую сотрудника, в которой полей, так, пятьдесят? По этой же причине отпадают INI файлы. Lua-конфиги очень интересны и интегрируются достаточно просто, добавляя при этом море возможностей. Но, опять же, заводить виртуальную машину ради парсинга текстового файла не хотелось.

Поэтому, в конце концов, выбор пал на JSON. Достаточно распространённый. Достаточно масштабируемый. Лёгкий в редактировании и освоении (на случай, если редактировать его придётся околоайтишному специалисту).

Так, с форматом определились, осталось найти парсер этого формата абсолютно без каких-либо зависимостей… так, стоп. Единственное, что нашлось с таким «фильтром», это парсер jsmn. Но проблема в том, что это именно парсер. Он не формирует json-объекты, а только разбирает строку на токены. И если формат загружаемого *.json файла известен заранее, то можно достаточно просто получить все значения. Но стоп, если формат загружаемого файла известен заранее, то почему бы не использовать бинарный формат? Поэтому представим, что формат файла мы не знаем. Поэтому быстренько напишем свою обёртку над jsmn.

Так и родился проект Json For Embedded Systems (JFES).

Основные возможности


  • Совместим с C99
  • Абсолютно никаких зависимостей.
  • Легко портируем.
  • Можно использовать только как парсер.


Основой библиотеки JFES являются два файла: jfes.h и jfes.c, а объектом, вокруг которого всё крутится, является jfes_value_t.

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


В свою очередь, поле type может принимать значения:

/** 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;


А поле data — это 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;


Инициализация


Чтобы инициализировать библиотеку, надо инициализировать объект jfes_config_t.

/** 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;


Помните я сказал, что JFES полностью без зависимостей? Так и есть, сама она даже выделять память не умеет и в её исходниках вы не найдёте ни одного #include. Вы можете указать свои функции выделения памяти, если хотите, в целях отладки, проверять выделяемую память, или из-за того, что у вас и есть только ваши функции управления памятью.

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


Всё! После этого вы можете парсить json-строку как вам угодно. Любое значение вы можете редактировать, а после этого сохранить опять в строку. Небольшой пример работы с JFES.

Мы будем парсить такой 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 ]
}


Парсить его будем с помощью этого кода:

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);


Послесловие


Библиотека не идеальна, как и любой другой код. В ней есть ошибки, недочёты, опечатки. Но в этом сила Open Source. Буду рад любым pull-request`ам, замечаниям, тикетам и пожеланиям.

Ссылка на мой github: JFES

Надеюсь, что хоть кому-нибудь она окажется полезной и сэкономит неделю велосипедостроительства. По мере своих сил и возможностей, буду её дорабатывать. Все nightly-коммиты будут в ветке experimental, в master будет мержиться более-менее стабильный и протестированный код. Исключение будет только для alpha-стадии — пока продукт будет в alpha, возможны поломки master ветки, но я буду стараться ничего не ломать и всё равно использовать experimental максимально возможно.

До версии 1.0.0 API может меняться, будьте внимательнее. Но об изменениях API будет написано в описании commit`a.

Огромное спасибо zserge за библиотеку jsmn.

Успехов!

© Habrahabr.ru