CJSON — C JSON Parser
Librairie C de manipulation JSON — parsing récursif, getters typés, modification en place et sérialisation — écrite sans libc ni dépendances externes pour être réutilisée dans plusieurs projets Epitech.
Contexte
En première année à Epitech, les règles sont strictes : la libc est interdite, tout comme les librairies externes. Pas de strlen, strcmp, strcpy, pas de dépendances tierces — seules quelques fonctions système de bas niveau sont autorisées (write, read, malloc, free...), et la liste varie selon les projets.
Plutôt que de réimplémenter les utilitaires manquants à chaque projet, j'ai choisi de construire une librairie réutilisable, avec uniquement les primitives autorisées en socle. CJSON est né de ce besoin concret : fournir un parser JSON complet en C pur, sans dépendances externes, utilisable dans plusieurs projets.
L'objectif principal était de l'intégrer dans MyRPG, le projet de fin d'année — un jeu développé en équipe en CSFML. L'idée : ne plus jamais recompiler le jeu pour ajuster un paramètre, charger les configurations depuis des fichiers JSON, sauvegarder et recharger les parties, et même synchroniser des états entre instances réseau.
Architecture
Le système de types
Le cœur de la lib repose sur une structure récursive capable de représenter n'importe quelle valeur JSON :
typedef struct s_cjson {
char *key;
cjson_value_t value;
cjson_type_t type;
cjson_t *prev;
cjson_t *next;
} cjson_t;cjson_t est à la fois un nœud d'objet JSON et un maillon de liste chaînée. La valeur est une union qui couvre tous les types JSON :
typedef union s_cjson_value {
cjson_t *v_object;
cjson_array_t *v_array;
char *v_string;
float v_number;
bool v_bool;
cjson_null_t v_null;
} cjson_value_t;Les arrays sont des listes chaînées de cjson_t :
typedef struct s_cjson_array {
cjson_t *first;
cjson_t *last;
size_t len;
} cjson_array_t;Cette représentation permet un parsing récursif naturel : un objet imbriqué n'est qu'un cjson_t dont la valeur est un autre cjson_t.
Le parser
Le parsing est récursif descendant, caractère par caractère, sans libc. Chaque type JSON a son propre sous-parser dans src/parse/values/ — string.c, number.c, array.c, object.c, bool.c, null.c. Les nombres sont gérés manuellement : partie entière, partie décimale et signe stockés séparément avant conversion en float.
typedef struct s_internal_cjson_number {
bool negative;
bool dot;
long whole;
long decimal;
long divider;
} internal_cjson_number_t;Utilisation
Charger un fichier JSON
#include "cjson.h"
cjson_t *config = cjson_parse_file("config.json");Pour un fichier comme :
{
"id": 3,
"price": 1.90,
"name": "John",
"enabled": true,
"notes": [2, 4.5, 78, 12]
}Accès aux propriétés
Deux modes : accès direct (sans vérification d'erreur) ou accès sécurisé via paramètre de sortie.
// Accès direct — pratique mais sans gestion d'erreur
int id = cjson_get_prop_int_unsafe(config, "id");
float price = cjson_get_prop_float_unsafe(config, "price");
char *name = cjson_get_prop_string_unsafe(config, "name");
bool enabled = cjson_get_prop_bool_unsafe(config, "enabled");
// Accès sécurisé — recommandé
int id = 0;
if (!cjson_get_prop_int(config, "id", &id))
handle_error();Modification en place
cjson_t *prop = cjson_get_prop(config, "price");
cjson_set_prop_key(prop, "cost"); // renomme la clé
cjson_set_prop_value(prop, CJSON(34.7f), CJSON_NUMBER_T); // modifie la valeur
cjson_set_prop_value(prop, CJSON(false), CJSON_BOOL_T); // change même le typeConversion d'arrays
cjson_array_t *notes = NULL;
cjson_get_prop_array(config, "notes", ¬es);
size_t len = 0;
int *values = cjson_array_to_int_array(notes, &len);
// values = [2, 4, 78, 12], len = 4API publique
| Fonction | Description |
|---|---|
cjson_parse_file(path) | Parse un fichier JSON, retourne un cjson_t * |
cjson_get_prop(obj, key) | Retourne le nœud JSON pour une clé |
cjson_get_prop_int(obj, key, &out) | Getter sécurisé — int |
cjson_get_prop_float(obj, key, &out) | Getter sécurisé — float |
cjson_get_prop_string(obj, key, &out) | Getter sécurisé — string |
cjson_get_prop_bool(obj, key, &out) | Getter sécurisé — bool |
cjson_get_prop_array(obj, key, &out) | Getter sécurisé — array |
cjson_set_prop_key(node, key) | Renomme la clé d'un nœud |
cjson_set_prop_value(node, val, type) | Modifie la valeur et le type d'un nœud |
cjson_array_to_int_array(arr, &len) | Convertit un array JSON en int[] |
cjson_stringify(obj) | Sérialise un objet JSON en string |
cjson_export(obj, path) | Écrit un objet JSON dans un fichier |
cjson_display(obj) | Affiche un objet JSON formaté |
Utilisation dans MyRPG
CJSON a rempli trois rôles dans le projet de fin d'année :
- Configuration — paramètres du jeu (vitesse des entités, dimensions des maps, placement des PNJ) lisibles et modifiables sans recompilation
- Sauvegarde — état du joueur, progression, inventaire sérialisés sur disque entre deux sessions
- Réseau — sérialisation des messages échangés entre instances pour la fonctionnalité multijoueur
Ce dernier usage a révélé une limite réelle : en première année, sans avoir encore étudié le réseau, la synchronisation réseau en temps réel avec une lib de parsing conçue pour des fichiers statiques montrait ses limites de performance. La feature a fonctionné, mais la librairie a montré ses limites quant à la vitesse de sérialisation / déserialisation.
Limites et recul
La lib ne gère pas les null en tant que valeur explicite dans tous les chemins de code, et la gestion des erreurs de parsing n'est pas toujours remontée proprement à l'appelant. La représentation des nombres en float est aussi une simplification : pas de support pour les grands entiers, précision limitée. Ce n'était pas un problème pour les usages visés, mais ce serait un des points à retravailler.
Ce projet a accompagné le développement de notre projet de fin d'année — c'était un bon exercice de rigueur et nous a permis de rendre le projet plus flexible et maintenable. Cependant, pour un usage plus intensif ou des besoins de performance plus élevés, une refonte complète serait nécessaire : meilleure gestion de la mémoire, support des erreurs, et une architecture plus orientée vers la performance plutôt que la simplicité d'implémentation.