F.E.I.S est mon tout premier gros projet personnel. C'est un logiciel de bureau, cross-plateforme. Il est écrit en C++ avec SFML et Dear ImGui comme bibliothèques principales.
Les "caractères spéciaux" dans les chemins n'ont jamais marché dans mon code et j'ai mis très très longtemps à apprendre et comprendre pourquoi, et à trouver une solution qui me convienne.
Comment j'en suis arrivé là
~ Déni ~
J'aime l'API de std::filesystem
, elle me rappelle la chaleur et la douceur de mon pathlib
natal en Python.
Je voulais utiliser std::filesystem::path
dans F.E.I.S pour :
- Manipuler et stocker des noms de fichiers et chemins
- Ouvrir, lire et écrire des fichiers
Eh bah ça a été un tel BORDEL de juste parvenir à faire ces actions simples en gardant un code cross-plateforme que ça a suffi à me motiver à écrire cet article.
Sous Linux (OS avec lequel je code et teste), tout va bien, je peux ouvrir un fichier avec un chemin comme :
/home/syméon/charts/ありふれたせかいせいふく.memon
et tout roule, ça s'ouvre, ça se lit bien, tout va bien. (remarquez le é
et les caractères japonais)
Par contre sous Windows, que dalle. On me dit au choix que le fichier n'existe pas, ou qu'il est complètement vide.
Aux racines du mal (encodé)
~ Colère ~
Ça commence à devenir un running gag dans mes articles, mais le problème au coeur de tout ça c'est encore un doux mélange de problèmes d'encodages et de spécificités Windows.
Du peu que j'ai compris, la lib standard C++ vous laisse sur le bas-côté dès qu'il s'agit de faire un truc un peu concret, comme d'habitude.
Peu importe la manière dont vous décidez d'ouvrir un fichier en C++ (fstream
ou fopen
), votre implémentation de la lib standard doit derrière utiliser l'API de votre OS pour réellement ouvrir ce fichier.
Je vais pas faire semblant que je comprends très bien ce qui se passe ici, mais en gros sous Linux y'a peu de problèmes :
- vous pouvez stocker des chemins et noms de fichiers encodés en utf-8 dans des
std::string
- si vous le faites, la conversion
std::string
↔std::filesystem::path
se fait bien - de haut en bas de l'OS ça marche plutôt bien avec de l'utf-8, de la GUI jusqu'au syscall
Sous Windows, c'est une bonne salade de débris de verre :
- vous pouvez stocker des chemins et noms de fichiers encodés en utf-8 dans des
std::string
, dans le sens ou vous pouvez aussi bouffer du sable, si ça vous chante. - si vous le faites, la conversion
std::string
↔std::filesystem::path
ne se fera pas correctement, du moins sans un effort très conscient du problème qu'il faut contourner. - même si vous arrivez à corriger ça, Windows possède deux API d'ouverture de fichier, une seule "moderne" qui supporte réellement les "caractères spéciaux", et on dirait qu'aucune implémentation de la lib standard C++ ne l'utilise, pour une raison que j'ignore. Il est donc impossible d'ouvrir un fichier avec des caractères spéciaux dans son nom en passant une
std::string
à un autre bout de la lib standard C++
Ça fait extrêmement mal de voir un langage qui se prend autant au sérieux que C++ se prendre les pieds dans le tapis avec une telle violence. La "solution" officielle proposée en C++ c'est de faire du code spécial Windows super moche avec des concepts purement Windows qui n'ont pas leur justification ailleurs genre convertir et/ou manipuler des chaines de caractères de deux types, certaines avec encodage "étroit" et d'autres "large" etc ...
Bref, allez ennuyer quelqu'un d'autre avec vos mauvais choix de design. On est en ${current_year}
, utf-8 partout ou rien, pas de commentaires.
Ma solution
~ Marchandage ~
Quand il faut utiliser des std::string
qui représentent des noms de fichiers ou des chemins, assurez-vous qu'elles contiennent bien de l'utf-8.
Ajoutez ces deux petites fonctions dans votre code pour convertir correctement des std::string
en std::filesystem::path
et vice versa :
std::filesystem::path string_to_path(const std::string& utf8s) {
const std::u8string u8s{utf8s.cbegin(), utf8s.cend()};
return std::filesystem::path{u8s};
}
std::string path_to_string(const std::filesystem::path& path) {
const auto u8s = path.u8string();
std::string result{u8s.cbegin(), u8s.cend()};
return result;
}
Elles supposent que vos std::string
contiennent bien de l'utf-8 et passent par des std::u8string
pour forcer les "bonnes" conversions, peu importe l'OS.
Relisez tous les bouts de votre code qui manipulent des std::string
ou des std::filesystem::path
et modifiez-les si besoin (y'en a très souvent besoin) pour qu'ils utilisent ces fonctions.
Stockez et manipulez des std::filesystem::path
dans votre code, pas des std::string
Utilisez la bibliothèque nowide pour ouvrir vos fichiers, je recommande la branche standalone pour ne pas devoir vous farcir tout boost avec.
Pour ouvrir un fichier, donnez à nowide
vos std::filesystem::path
reconvertis en std::string
avec vos petites fonctions.
Patchez vos libs pour qu'elles utilisent nowide
, des fois c'est facile des fois non.
Bref
~ Dépression et Acceptation ~
C++ est une immense blague, on programme en C++ quand on a un problème d'égo, quelque chose à prouver aux autres. J'essaye moi-même d'en guérir.