Вбудувати 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.

Успіхів!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.