Tous les projets
2023GitHub

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.

CJSONParsingEpitech

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 type

Conversion d'arrays

cjson_array_t *notes = NULL;
cjson_get_prop_array(config, "notes", &notes);

size_t len = 0;
int *values = cjson_array_to_int_array(notes, &len);
// values = [2, 4, 78, 12], len = 4

API publique

FonctionDescription
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 :

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.