La magie du C++

Il m'arrive de déconsidérer le C++ par certains aspects laids (qui ont pour but de garder une compatibilité avec le C), mais en l'occurence je suis satisfait d'avoir fait un petit système très joli (note: je ne parle pas d'Anicée) grâce à la magie du C++.

Je suis actuellement sur WebRhap v2, un logiciel propriétaire à vocation commerciale pour la société RDL.

J'avais écris à l'origine une première version avec des outils pour Windows, mais après quelques considérations il me devenait inévitable d'écrire une version portable. C'est donc ce que je fais, non sans peine, car je dois le faire rapidement, et bien que je peux réutiliser les morceaux de code de haut niveau (basées sur des classes que je peux réécrire sans en changer l'interface), je m'aperçois d'erreurs de conception, ou j'ai l'idée d'écrire des trucs plus élaborés.

C'est ainsi que j'en suis venu à penser à un mega système de logs de la mort qui tue, qui n'a rien de révolutionnaire en soit, mais qui est, je trouve, très élégant à l'utilisation.

Ma problématique se base sur le fait que je voulais utiliser les flux, car j'en ai marre de passer par des valist, ce qui m'oblige, lors du passage de variables de type std::string, à appeler l'attribut c_str() renvoyant le char*, ce qui rends très laid.

J'ai donc commencé à me pencher sur les flux, mais voulant utiliser des flags pour décrire tel ou tel niveau de gravité (sachant qu'on pourrait combiner des flags pour avoir plusieurs comportements simultanément), il me fallait trouver un moyen de donner un argument au flux.

J'ai donc au final opté pour un mega système très joli mais qui n'apporte absolument rien de plus que le traditionnel Debug(int flags, const char* pattern, ...);, mais qui m'excite particulièrement.

TDebug

J'ai d'abord une classe TDebug qui a pour unique but d'être instanciée en tant que Debug au lancement du programme comme variable globale.

 
class TDebug
{
/* Constructeur */
public:
 
	TDebug();
 
	~TDebug();
 
/* Attributs */
public:
 
	void SetLoggedFlags(std::string s);
	unsigned int LoggedFlags() const { return logged_flags; }
 
/* Private */
private:
 
	unsigned int logged_flags;
};
 

Rien de spécial. La variable logged_flags sert à garder les flags qu'on logue (paramétrable dans la conf). On constate que SetLoggedFlags(std::string s) récupère une chaîne. En effet, cette fonction parse directement ce qu'on lui donne comme labels dans la conf, et met les flags par lui même.
Mais on ne va pas s'y intéresser ici, d'autant plus que ça n'est pas intéressant.

TDebug::flux

L'idée est qu'une sous structure va servir de flux, et on l'obtient par l'opérateur [].

Voici le code tout ce qu'il y a de plus con (dans TDebug) :

 
	flux operator[](size_t __n)
	{
		return flux(__n);
	}
 

Lorsque l'on appelle Debug[i], on obtient un objet de type TDebug::flux qui va capturer lui même l'opérateur << :

 
	class flux
	{
		std::string str;
		size_t flag;
 
	public:
		flux(size_t i)
			: flag(i)
		{}
 
		~flux();
 
		template<typename T>
		flux& operator<< (T s)
		{
			std::ostringstream oss;
			oss << s;
			str += oss.str();
			return *this;
		}
	};
 

Ceci mérite quelques explications.

Tout d'abord, on a dans un std::string une chaîne, et dans un size_t les flags correspondants. Jusque là ça va.

Le constructeur stocke le flag passé, et le destructeur est implémenté dans Debug.cpp et on en parlera plus tard.

Intéressons-nous à operator<<. J'ai utilisé un template dans le but de récupérer n'importe quel type T, de le passer à un std::ostringstream afin de profiter de sa capacité de conversion et de récupérer la chaîne via l'attribut str().

Notez qu'on renvoie une référence de nous même. Ce qu'il faut préciser, c'est que dans une instruction de ce genre :

 
   std::cout << "blah " << i << " bla bla" << std::endl;
 

On évalue d'abord std::cout << "blah ", qui renvoie un objet de type std::ostream, afin d'évaluer ensuite std::ostream << i, et ainsi de suite. La rencontre de std::endl; ajoute une fin de ligne et fait un flush (il existe d'ailleurs std::flush).

Ici, on ajoute à str à chaque fois la chaîne qu'on récupère. Et ainsi que vous l'aurez compris, à la fin de l'exécution de l'instruction, notre TDebug::flux est détruit, et c'est là qu'on fait notre flush.

Syslog

Ainsi que j'ai omis de le préciser au début, probablement du fait que je reflechis au fur et à mesure que j'écris et que j'ai la flemme de corriger le début, je me sers de syslog pour logger.

Vous vous doutez donc maintenant de l'implémentation du constructeur TDebug::TDebug et du destructeur TDebug::~TDebug :

 
TDebug::TDebug()
	: logged_flags(DEFAULT_LOGGED_FLAGS)
{
	openlog("WebRhap", LOG_CONS, LOG_DAEMON);
}
 
TDebug::~TDebug()
{
	closelog();
}
 

Le destructeur de TDebug::flux sera pas beaucoup plus compliqué :

 
static struct
{
	int flag;
	int level;
	const char* s;
} all_flags[] =
{
	{ W_DEBUG,      LOG_DEBUG,   "DEBUG"      },
	{ W_PARSE,      LOG_DEBUG,   "PARSE"      },
	{ W_CMDS,       LOG_INFO,    "CMDS"       },
	{ W_WARNING,    LOG_WARNING, "WARNING"    },
	{ W_BIGWARNING, LOG_WARNING, "BIGWARNING" },
	{ W_ERR,        LOG_ERR,     "ERR"        },
	{ W_CONNEC,     LOG_NOTICE,  "CONNEC"     },
	{ W_MODIF,      LOG_NOTICE,  "MODIF"      },
	{ W_WEBSITE,    LOG_NOTICE,  "WEBSITE"    }
};
 
TDebug::flux::~flux()
{
	int i;
 
	if(!(flag & Debug.LoggedFlags()))
		return;
 
	for(i = (sizeof all_flags / sizeof *all_flags) - 1; i >= 0 && !(flag & all_flags[i].flag); --i);
 
	if(i < 0)
		syslog(LOG_WARNING, "[SYSLOG] (%X) Unable to find how to log this message: %s", flag, str.c_str());
	else
	{
		syslog(all_flags[i].level, "[%s] %s", all_flags[i].s, str.c_str());
		#ifdef DEBUG
			std::cout << "[" << all_flags[i].s << "] " << str << std::endl;
		#endif
	}
}
 

Tout d'abord on vérifie qu'on logue bien ce flag, ensuite on cherche dans le tableau de structure all_flags. Si ça n'est pas trouvé, on met un message pour dire que c'est la mémerde, et sinon on logue en prenant le level spécié, le label, et le message.

Spécialisation de templates

C'est là que j'en ai un minimum chié, et non pas parce que c'est compliqué, mais tout simplement du fait que G++ est un peu bizare sur quelque chose apparament pas spécifié dans les normes C++ (à confirmer).

Tout d'abord, je souhaitais spécialiser std::string pour éviter qu'il passe inutilement par un flux si il lui suffisait d'utiliser l'opérateur += de str.

Le code est trivial :

 
		template<>
		flux& operator<< <std::string> (std::string s)
		{
			str += s;
			return *this;
		}
 

Si je mets ce code dans TDebug::flux juste après la définition du template operator<<(T s), g++ m'envoie chier car il ne souhaite pas que je définisse la spécialisation dans la classe directement mais en dehors.

Je définis donc en dessous de la définition de TDebug (en rajoutant les namespaces comme il faut), et il m'envoie chier lors de l'édition des liens car il a plusieurs références à la même fonction (même signature). C'est naturel, mais du coup je me sens emmerdé.
La solution a été tout simplement de mettre la fonction en inline, comme suit :

 
template<>
inline TDebug::flux& TDebug::flux::operator<< <std::string> (std::string s)
{
	str += s;
	return *this;
}
 

Du coup ça marche, et le code étant on ne peut plus faible, le passage en ligne de la fonction n'est pas un problème.
Ce qui est étrange c'est qu'il semblerait que G++ soit le seul compilateur à empecher de définir une spécialisation au sein même de la définition de la classe, ce qui me semble étrange, car je n'en vois pas trop l'intérêt.

Rajouté la spécialisation pour const char* qui suit le même principe, je ne vais pas le recoller tout de même.

Par contre là où ça devient très intéressant, c'est une spécialisation sur des objets spécifiques pour les mettre dans les logs sans trop d'efforts.
Voici l'exemple pour la classe Client :

 
template<>
inline TDebug::flux& TDebug::flux::operator<< <Client*> (Client* cl)
{
	str += "(";
	str += odbc2str(cl->Ip());
	str += ") ";
	return *this;
}
 

Passons sur l'appel à odbc2str(), car une contrainte est que WebRhap puisse supporter l'unicode, et donc passer par des chaines de caractères étendues (wchar_t*), ce qui m'oblige pour des appels à des fonctions externes n'ayant pas d'équivalentes pour les wchar_t*, à faire des conversions. odbc pour préciser que c'est une chaine de "type" ODBCXX_STRING (ce qui est juste un alias vers std::string ou std::wstring en fonction de la définition ou non de ODBCXX_UNICODE).

Fin

Tout ça pour arriver à un résultat relativement élégant :

 
/* Par exemple */
Debug[W_PARSE] << "S" << cl << "- " << buf;
 
/* Ou */
Debug[W_BIGWARNING] << "-" << fd << "- This fd is already used ! (connexion from " << ip << ")";
 

Ce qui va afficher dans daemon.log par exemple :

Aug  4 17:47:26 laptop WebRhap: [PARSE] S(127.0.0.1) - HEL WRW 4 kikoo lol^W
Aug  4 17:47:26 laptop WebRhap: [BIGWARNING] -4- This fd is already used ! (connexion from 127.0.0.1)

Bon maintenant que je suis arrivé à ce résultat, il me serait plaisant d'écrire dans un unique fichier webrhap.log plutôt que de squatter daemon.log, syslog.log et debug.log... Mais j'ai pas encore eu le temps de m'y pencher, car bon c'est cool de faire un joli système de logs, mais c'est pas la fonction principale du programme, et je suis encore loins du but !

P.S.: Dédicasse spéciale à toi, petit con, qui a lu ce post jusqu'au bout.

8 Responses to “La magie du C++”


  • J’ai rien compris xD
  • Par rapport aux fonctions strcat, printf, etc. du C, les flux C++ sont une grosse avancée en particulier grâce à leur généricité. Tout passe par les « interfaces » ostream et istream : std::cout hérite de ostream par exemple. Pour pouvoir écrire son objet dans cout, il suffit d’écrire un opérateur du genre « ostream& operator »
  • Humpf, pas tr`es Unicode compliant ce code et ce blog :-p
  • Grâce à mon extrème générosité, j’ai modifié ton commentaire pour le rendre lisible.
    Par contre je ne comprends pas pourquoi lors de l’édition d’un nouveau commentaire sur la page d’un billet ça foire, alors qu’en éditant par l’interface d’administration ça passe bien. Faudrait que je regarde.

    En effet les flux C++ sont très pratiques. On peut cependant regretter l’absence d’encapsulation des sockets dans la STL, mais je suppose que la lib boost le fait (bien que je n’ai jamais utilisé cette dernière).
    Et effectivement, ce qui est extrèmement pratique c’est l’extensibilité par la définition d’opérateurs < <.

    Par contre en l’occurence, je ne pouvais pas utiliser std::cout en raison du fait qu’on passe par syslog et qu’on doive flusher à la destruction de l’objet TDebug::flux.

  • Pour les commentaires c’est peut-être à cause d’un truc totalement useless du thème que j’ai mis, à savoir les commentaires AJAX (qui s’affichent sans recharger la page). Je les ai virés en attendant.

    Pour les logs, il y a moyen de faire des filtres, suivant le logiciel utilisé (syslog-ng, que j’utilise et qui a une configuration imbitable mais quand ça marche on jouit, ou metalog). A noter qu’Ubuntu (en tout cas la Server) est fournie avec un vieux syslog moisi du gland, je ne sais pas si c’est le cas de Daubian aussi.
    Enfin bon l’intérêt de syslog c’est principalement d’avoir plusieurs programmes qui peuvent écrire « en même temps » dans un fichier, si tu veux que webhrap aie son propre fichier, et que tu n’as pas plusieurs processus, autant coder ton propre logger.

  • Le problème est que WebRhap devrait être lancé depuis un compte user, et écrire dans /var/log qui appartient à root. Donc syslogd me permettait d’écrire dedans. Je ne sais pas quelle autre solution pourrait être utilisée pour écrire dans /var/log…
  • Crée le fichier log auparavant, appartenant au user:groupe (suffit de faire un petit script à lancer en root). Et bien sûr drwxr-xr-x root root pour les permissions de /var/log, mais ce devrait être déjà le cas. Si tu veux avoir plusieurs logs, crée un dossier dans /var/log avec la même méthode, comme ça webrhap pourra faire ce qu’il veut dedans.
  • Leave a Reply


    Catégories



    Bear