Stockage de paramètres

problème de conception

a marqué ce sujet comme résolu.

Bonjour,

Je développe des systèmes embarqués. j'utilisais le C, mais, maintenant que les microcontrolleurs sont puissant (le miens tourne a 192MHz, à 128ko de ram et 2Mo de flash) je suis passé sur le C++(11). Je ne suis donc pas encore un "guru" C++.

En C je stockais les paramètres généraux de mon programme dans une grosse struct, que j’insérais en flash, et que je j'allais chercher au démarrage. A chaque que fois que je rajoutais un paramètre, il fallait mettre à jour la structure, mettre en place une méthode pour le chargement en prenant en compte le fait que la structure en flash était peut être une ancienne version, bref, pas très "powerful" (mais sa marche).

Avec le C++, j'ai eu une idée. Une structure générique comme ceci :

1
2
3
4
5
6
7
struct Parameter_t
{
    Parameter_e m_id;
    Type_e m_type;
    char m_key[20];
    void* m_value;
};

Parameter_e et Type_e sont des enum. J'ai une classe "Parameter" qui contient un std::vector<Parameter_t>. J'ai ensuite un setter pour chaque type que je pourrais rencontrer :

1
2
3
Parameter::Set(Parameter_e p_param_e, uint32_t p_value);
Parameter::Set(Parameter_e p_param_e, const char * p_value);
//etc...

Et pour le getter c'est un peu plus complexe, j'ai fait une template :

1
2
3
4
5
template<typename T>
T Parameter::Get(Parameter_e p_param_e)
{
    return *(static_cast<T*>(Parameter::getInstance()->m_parameterList[p_param_e].m_value));
}

(Parameter est un Singeleton, mais ce n'est pas vraiment important). On doit connaitre le type du paramètre, que l'on va chercher, mais ce n'est pas très grave.

En pratique cela fonctionne mal, il faudrait faire une map, mais avec des type différent c'est pas vraiment possible. Une des solutions aurait été de tous convertir en chaine de caractères. Même si j'ai un embarqué assez puissant, cela reste un embarqué, j'aimerais ne pas trop alourdir la bête.

Si vous avez une idée élégante pour ce type de problème… J'ai vu des choses sur le type erasure, mais je n'ai pas compris comment je pouvais le mettre en oeuvre.

d'avance merci.

Lu'!

C++ est un langage plus typé que C. En l'occurrence, ton usage de void* me semble quelque peu douteux ;) .

Je ne suis pas sûr de comprendre le problème concret que tu essaies de résoudre. Si tes éléments sont de type différents, il est peu probable qu'il soit pertinents de les stocker ensemble car ils n'ont pas le même usage. Et si tu dois t'amuser à tester chaque élément pour déterminer son usage, tu vas vite te retrouver avec une usine à gaz. Donc ce serait bien que tu précises un peu ton but.

PS : Le singleton est le plus traître des patterns car il est rarement justifié et va introduire beaucoup de couplage et de rigidité dans le design, il faut vraiment l'utiliser avec parcimonie.

Je vais essayer de reformuler mon problème.

Mon programme à un ensemble de paramètres nécessaire à sont bon fonctionnement. On peut faire le parallèle entre mon besoin et, disons, le logiciel Apache HTTP (le serveur web). A chaque démarrage, Apache va chercher un fichier de configuration (par défaut httpd.conf). En fonction de ce httpd.conf, le comportement d'Apache change. Et bien c'est exactement la même chose avec mon programme. Sauf que je n'ai pas le "luxe" d'avoir un système de fichier.

Effectivement, je n'ai pas trouvé comment faire actuellement pour faire quelque chose de "propre" qui ne ne ressemble pas à une usine a gaz. D'où ma question :D.

En ce qui concerne void*, je l'utilise car c'est le type le plus générique qui soit.

Enfin, je suis bien conscient de la non flexibilité qu'apporte le singleton, mais je pense que le singleton est ici adapté à une classe d'accès au données. J'utilisais les singleton pour les accès au base de données dans d'autre langage, pour moi, "Parameter" accède à des données (peut importe où) et cette ressource est unique, d'où le singleton. Mais peut être ai-je tort sur ce point.

En ce qui concerne void*, je l'utilise car c'est le type le plus générique qui soit.

Mr Mi

Je verrais plutôt ça comme le type qui n'en est pas un. On ne mélange pas les torchons et les serviettes et c'est justement ce que permet void*. Quand on a besoin de type-erasure, il est plus sage de se tourner vers boost::any mais encore une fois il faut avoir une bonne raison de faire cette suppression de type parce que c'est hyper casse gueule de revenir à un état cohérent.

Il y a clairement deux tâches à séparer : l'extraction des données brutes et leur transformation en un ensemble de variables dans le programme. La première tâche c'est de la désérialisation qui part de données brutes vers les bons types, la seconde c'est le passage de ces éléments bien typés vers leurs variables d'accueil.

Si on veut continuer le parallèle avec les fichiers de configuration, dans ce genre de cas, on choisirait de passer par boost::program_option (par exemple) et tout ce qu'on obtient en sortie de ce machin est typé. Et ce que fait ce matos c'est exactement les deux tâches précédentes.

En fait, tes données ne devraient pas, au sein de ton programme, être stockées comme des éléments non-typés à quelque moment que ce soit, sauf éventuellement à fins d'optimisation (je bufferise en non-typé, puis je flush). Ce que tu veux, c'est pour chaque élément de ton programme qui doit être enregistré, une solution de sérialisation/désérialisation, tu peux regarder boost::serialize, s'il ne fait pas tout ce que tu veux, inspire toi au moins de son interface. Ensuite, la sauvegarde/lecture de paramètre, ça peut ressembler à :

1
2
3
4
5
template<class T>
void save_parameter(id identifier, T value);

template<class T>
T load_parameter(id identifier);

Et ces fonctions appelleraient les fonctions de sérialisation.

J'utilisais les singleton pour les accès au base de données dans d'autre langage, pour moi, "Parameter" accède à des données (peut importe où) et cette ressource est unique, d'où le singleton.

Mr Mi

Moui. Les données sont uniques de toute façon. Mais rien n'empêche d'avoir de multiples points d'accès à ces données. En mettant un singleton, on croit simplifier le bousin parce qu'on a qu'un seul point d'accès mais en fait on y gagne rien, pourquoi ?

  • c'est global donc de toute façon, même s'il n'y a qu'un point d'accès, si on y accède d'un peu partout ce sera le même bordel intraçable,
  • on ne peut pas sélectionner précisément les endroits où l'on autorise l'accès parce que dès inclusion du header tout le fichier peut y accéder sans discrimination.

Avec une classe de fournisseur d'accès, on peut créer des points d'accès locaux, on aura toujours une certaine difficulté à tracer les appels mais finalement beaucoup moins parce que les parties qui auront droit à un point d'accès seront beaucoup plus faciles à trouver : c'est celles qui possèdent une instance de point d'accès.

Je vois mal ce que vient faire program_option sur un micro contrôleur ou y'a pas de filesystem, pareil pour boost::serialize.

Mais pour une meilleure réponse, il faudrait voir comment tu mets à jour tes paramètres et comment tu lis ces valeurs. Car s'ils connus à la compilation et ne change pas, de bonne grosse variables globales dans un namespace et c'est plié.

boost::any peut servir ici. En gros, avec boost::any tu peux stocker n'importe quel valeur (c'est comme du void* mais en beaucoup moins sale). La technique s'appelle le type-erasure. Il faut à l'avance connaitre le type que tu veux récupérer à partir de l'any puisque tu perds totalement cette information. Il faut aussi voir si ton système supporte les exceptions, car dans ce cas, tu devra utiliser une implémentation faite maison (celle de boost en lance).

Il faut voir l'overhead, je ne sais absolument comment est codée boost::any

Enfin, si jamais tes paramètres sont placés à une adresse précise, une solution dégeu mais qui marche c'est de faire du placement new de variable globale constante à cette adresse.

+0 -0

Je vois mal ce que vient faire program_option sur un micro contrôleur ou y'a pas de filesystem, pareil pour boost::serialize.

Davidbrcz

J'avais pour idée de regarder l'interface et de m'en inspirer, je ne peut pas vraiment me permettre boost (déjà que je me permet std::string que dans de très rare cas).

Je voulais éviter les variables globales, c'est déjà ce que j'ai en C, et à chaque évolution, il faut écrire une fonction "LoadFromV0X" quand je passe à la version X+1.

La technique s'appelle le type-erasure. Il faut à l'avance connaitre le type que tu veux >récupérer à partir de l'any puisque tu perds totalement cette information.

Davidbrcz

Si tu regarde ma structure de départ je garde cette info :

1
2
3
4
5
6
7
struct Parameter_t
{
    Parameter_e m_id;
    Type_e m_type;
    char m_key[20];
    void* m_value;
};

où Type_e est une enum contenant les types dispo pour les paramètres (UINT32, CHAR_STRING, UINT8, …)

Je vois qu'il n'y pas de solution miracle :D

Ca d'accord tu as une structure pour représenter un paramètre. Mais ce que je veux dire, c'est que tes paramètres sont :

  • Fixés une fois pour toi à la compilation ?
  • Doivent être lus depuis une mémoire quelconque ?
  • Si oui, ss tu la maitrise l'adresse à laquelle ils sont ?

Enfin bref, comment utilises tu cette structure et comment places tu les données que tu vas lire dessus.

+0 -0

les paramètres ont tous une valeurs par défaut. Au démarrage du système, On va tenter d'aller chercher dans la flash (à un endroit donnés, donc on connait effectivement l’adresse). Si cela fonctionne on copie les valeurs dans une structure de données en ram (un std::vector, un tableau, une struct, peu importe). Si ces valeurs sont modifiées pendant le fonctionnement du système (via des commandes externe), alors on met a jour la structure de données et on la pousse dans la flash.

C'est là que je ne comprends vraiment pas pourquoi tu as besoin de taper la tête contre les murs à faire des structures non-typées. Tes paramètres, tu connais leur type et si tu connais leur type, il suffit d'avoir pour chacun de ces types de fonction qui permet de le récupérer/le mettre dans la flash à partir de son adresse.

La structure de tes données en mémoire flash est la même que dans la ram ? Si c'est le cas, faut pas t’embêter a utiliser un tel système. Crées simplement une structure regroupant toutes tes variables (pour les utiliser dans ton programme) de façon a ce qu'elles soient contiguës en mémoire. Et tu fais un simple memcpy de tes donnes en mémoire flash vers la ram.

Si tu as des données en mémoire flash qui ne correspondent pas (ordre différent, liste de variables différentes, etc), cela me parait inutilement lourd d'utiliser un tel système de paramétrage. Tu pourrais simplement avoir quelques tableaux (par exemple un pour des int, un pour des float et un pour des strings). Et tu fais de la même façon un memcpy de tes données en flash vers la ram. La différence avec l'approche précédente est qu'il faut faire une étape de parsing lors du chargement, pour faire le lien entre tes données utilisées dans ton programme (des pointeurs ou références) vers les adresses mémoires de tes données chargées.

+0 -0

Je cherchais juste un moyen "maintenable" de stocker mes paramètres. Vos solutions sont simple, c'est ce que je fait actuellement. Leurs maintenances est pénible à souhait. Quand je rajoute un paramètres : - j'incrémente un compteur de version de ma structure "parameter" ; - je rajoute le paramètres, je lui fait un accesseur/getteur ; - j'écris une fonction pour gérer la migration entre une vieille version stocké en flash et nouvelle.

C'est simple, mais peut maintenable. Un tableau de paramètre, avec une seul fonction de sérialisation/désérialisation, se serait plus simple. Au final il me faudrait un équivalent d'un fichier .ini :D. Être pauvre c'est pas facile tous les jours…

@gbdivers : imagine que tu à plusieurs hard pour un même code métier, un memcpy t'oblige à t'occuper de la structuration mémoire de t'es struct. Sur certain compilateur (et même entre certaine version), l’alignement mémoire change, c'est plutôt pénible à gérer.

Avec les fonctionnalités du C++11 pour l'alignement, cela ne pose pas de problème. Mais tu n'y as peut être pas accès. Mais ce n'est pas grave, une simple copie classique fonctionnera. C'est surtout la structure de données (et le typage) qui compte

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte