Это страшное слово setlocale

История эта случилась достаточно давно, но до сих пор она продолжает преследовать меня страшными кошмарами, связанными в основном с локализациями наших замечательных игр. Об этих проблемах многие даже и не задумываются, а, тем не менее, они могут серьезно подорвать вашу уверенность в светлом будущем.

В начале разработки Недетских гонок у нас была «прогрессивная» по тем временам система балансировки параметров – существовала группа ini файлов, которые содержали строчки вида speed=1.25. В процессе игры можно было вызвать отладочное окно, в котором появлялась куча именованных трекбаров и, двигая ползунки, можно было в рантайме задавать разные параметры, отсматривать их в игре и записывать обратно в файл.

Система действительно была очень прогрессивной (по крайней мере, по сравнению с const float speed = 1.25), вот только мы не сразу додумались держать параметры под source control на локальном диске – до этого все параметры брались с сервера.

Как-то раз прибегает ко мне взмыленный программист и сообщает, что на сервере потерлись все параметры. Причем потерлись интересным образом – потерялись все дробные части коэффициентов. То есть тот же самый speed=1.25 превратился в speed=1. И так далее. Как обычно, я беру отладчик в зубы и начинаю проверяться.

Довольно быстро выясняется, что форматированная запись sprintf() работает как прежде.  Но вот только вместо десятичной точки при сохранении ini подставляется запятая. Быстрая проверка на старте игры показывает, что там сохраняется точка. Дальше методом половинного деления выясняется, что проблема заключается в инициализации библиотеки поддержки сети. Ну и бага проявляется только на ряде машин, на которых установлена сетевая карта такая-то, и если заблокировать сетевую карту в диспетчере устройств, то запятая не появляется.

После того, как в ini файл проникла запятая, следующее сканирование файла считывает его до пресловутой запятой, и вместо ожидаемой точной настройки переменной там оказывается обрезанное значение. Ну и последующее сохранение файла приводит к безвозвратной потере дополнительных значащих цифр.

Парился я с этой ошибкой довольно долго. Лазая по дизассемблерному sprintf(), дошел даже до инструкции, в которой брался символ из переменной __digitalpoint. В нем действительно была на старте игры записана точка, которая потом превращалась в запятую. Момент превращения был связан с подгрузкой в игру файла net.dll, который отвечал как раз за сетевую карту.

Только случайная идея помогла натолкнуться на решение проблемы. Оказалось, что баг не воспроизводится, если в Regional Settings заменить Russian на English. И действительно, для России разделителем считается запятая, для США – точка. Но почему какая-то долбанутая dll меняет мне точку в основном приложении.

Два простых теста позволяют понять, почему это происходило. Тест номер 1 дает возможность проверить это в игре:

[code]printf("Number: %f\n", 1.123817);
setlocale(LC_ALL, "Russian");
printf("Number: %f\n", 1.123817);[/code]

Тест номер 2 заключается в том, чтобы разместить setlocale() внутри dll-ки и посмотреть, что происходит. И тут начинается интересное.

Если кодогенерация приложения была Multithread, то __digitalpoint для exe и для dll файла – это разные переменные в разных файлах. Если кодогенерация приложения была Multithread Dll, то __digitalpoint для exe и для dll файла находится в одном и том же файле msvcrt.dll.

Фанаты плюсов спросят меня – а что будет, если воспользоваться std::cout? Ответ – она не зависит от локали и всегда пишет десятичную точку (Я проверял на MSVC 2005, можете взглянуть в Microsoft Visual Studio 8\VC\crt\src\xlocnum в строку 1104). Только вот хорошо это или плохо? И второй вопрос – зачем DLL файл вызывал setlocale() внутри себя?

Я, конечно, понимаю разработчиков драйверов. У них тоже есть баги, есть семьи, и надо сдавать проекты в срок. Поэтому они, не мудрствуя лукаво, взяли настройки Regional Settings из панели управления и передали их в CRT setlocale() чтобы им жилось хорошо. А вот зачем они это сделали – совсем другой вопрос.

Желающим разобраться в нем, могу предложить еще один тест. Сделайте Multibyte приложение следующего вида:

[code]int _tmain(int argc, _TCHAR* argv[])
{
{
std::ofstream os( "C:\\Игры\\test1.dat" );
}
setlocale(LC_ALL, "Russian");
{
std::ofstream os( "C:\\Игры\\test2.dat" );
}
return 0;
}[/code]

Потом создайте каталог «Игры» на диске C и запустите проект. Удивлены? А нет ничего странного.

Дело все в том, что если у вас до сих пор ANSI-шное приложение, то оно крайне недружелюбно ведет себя с современными операционками. Если вы получили char* указатель на имя файла (ну или std::string имя файла), то единственное, что вы можете сделать с ним – это дрожащими руками донести его до следующей функции, где оно используется и, помолясь, отдать его туда. Шаг вправо, шаг влево от этой секвенции – расстрел. Изменение настроек в Regional Settings во вкладке Language for non-Unicode programs – расстрел через повешение. Попытка применить MultiByteToWideChar() – обязательная молитва с выбором правильного CodePage.

Так а что же происходит тут? Зайдите в конструктор std::ofstream и пройдите до файла Microsoft Visual Studio 8\VC\crt\src\fiopen.cpp. Вызываемая из конструктора std::ofstream  функция _Fiopen(), оказывается, … внимание!… переводит строку методом mbstowcs_s(). А вот она уже зависит (почему то в отличие от std::cout) от текущей локали. И если у вас не дай бог в проекте встречается и std::ofstream, и MultiByteToWideChar() на текущую кодовую страницу Regional Settings, то без setlocale() вы ничего не загрузите.

Да, так что в итоге сделали мы? Мы тогда решили проблему просто – поменяли кодогенерацию с Multithread Dll на Multithread. Но Dll кодогенерацию я не люблю до сих пор. А проекты стараюсь писать на юникоде сразу. И даже на юникоде стараюсь не приближаться к std::ofstream – а то не ровен час, рванет в неожиданном месте – вдруг очередному мидлварю ну очень потребуется setlocale(LC_ALL, “French”)?

Ну и традиционно ненавижу библиотеки (CRT в том числе), чья работа критическим образом зависит от недетерминированных функций, которые вызвались много много минут назад, когда ваше приложение радостно шуршало диском и поднималось в оперативную память.

  • http://101gr.com/ GLoom

    Есть и более подлые локали. Например испанская (es-AR по крайней мере). У используется и запятые и точки. Одни отделяют тысячи, вторые – дробную часть (на вскидку не помню кто что отделяет).