Kill the locale dependency bug class by not allowing Qt to mess with LC_NUMERIC
.
Context: #18124 (Recommended reading if you are unsure about how C and C++ locales work in C++)
Guaranteed locale settings before this PR:
Program | C locale category LC_NUMERIC |
Global C++ locale (std::locale ) |
---|---|---|
bitcoind |
C (classic) |
C (classic) |
bitcoin-qt |
No guarantee - user-specified | C (classic) |
Guaranteed locale settings after this PR:
Program | C locale category LC_NUMERIC |
Global C++ locale (std::locale ) |
---|---|---|
bitcoind |
C (classic) |
C (classic) |
bitcoin-qt |
C (classic) |
C (classic) |
Background:
Perhaps surprisingly Qt runs setlocale(LC_ALL, "")
on initialization. This installs the locale specified by the user’s LC_ALL
(or LC_*
) environment variable as the new C locale.
In contrast bitcoind
does not opt-in to localization – no call to setlocale(LC_ALL, "")
(or std::locale::global(std::locale(""))
) is made and the environment variables LC_*
are thus ignored.
This results in an unfortunate situation where the bitcoind
is guaranteed to be running with the classic locale ("C"
) whereas the locale of bitcoin-qt
will vary depending on the user’s environment variables.
An example: Assuming the environment variable LC_ALL=de_DE
then the call std::to_string(1.23)
will return "1.230000"
in bitcoind
but "1,230000"
in bitcoin-qt
.
From the Qt documentation:
“On Unix/Linux Qt is configured to use the system locale settings by default. This can cause a conflict when using POSIX functions, for instance, when converting between data types such as floats and strings, since the notation may differ between locales. To get around this problem, call the POSIX function
setlocale(LC_NUMERIC,"C")
right after initializingQApplication
,QGuiApplication
orQCoreApplication
to reset the locale that is used for number formatting to “C”-locale.”
C and C++ locales 101:
0#include <clocale>
1#include <cstdlib>
2#include <iostream>
3#include <locale>
4#include <sstream>
5#include <string>
6
7int main(void)
8{
9 std::cout << "C locale at beginning of main: " << std::string{setlocale(LC_ALL, nullptr)} << "\n";
10 std::cout << "C++ locale at beginning of main: " << std::locale{}.name() << "\n\n";
11#if OPT_IN_TO_LOCALIZATION_USING_SET_LOCALE
12 setlocale(LC_ALL, "");
13#endif
14#if OPT_IN_TO_LOCALIZATION_USING_STD_LOCALE_GLOBAL
15 std::locale::global(std::locale(""));
16#endif
17 std::ostringstream oss;
18 oss << 1000000;
19 std::cout << "std::ostringstream oss; oss << 1000000; oss.str() equals " << oss.str() << "\n";
20 std::cout << "std::to_string(1.23) equals " << std::to_string(1.23) << "\n\n";
21 std::cout << "C locale at end of main: " << std::string{setlocale(LC_ALL, nullptr)} << "\n";
22 std::cout << "C++ locale at end of main: " << std::locale{}.name() << "\n\n";
23 std::cout << "setlocale(LC_COLLATE, nullptr) (read-only operation) = " << std::string{setlocale(LC_COLLATE, nullptr)} << "\n";
24 std::cout << "setlocale(LC_CTYPE, nullptr) (read-only operation) = " << std::string{setlocale(LC_CTYPE, nullptr)} << "\n";
25 std::cout << "setlocale(LC_MESSAGES, nullptr) (read-only operation) = " << std::string{setlocale(LC_MESSAGES, nullptr)} << "\n";
26 std::cout << "setlocale(LC_MONETARY, nullptr) (read-only operation) = " << std::string{setlocale(LC_MONETARY, nullptr)} << "\n";
27 std::cout << "setlocale(LC_NUMERIC, nullptr) (read-only operation) = " << std::string{setlocale(LC_NUMERIC, nullptr)} << "\n";
28 std::cout << "setlocale(LC_TIME, nullptr) (read-only operation) = " << std::string{setlocale(LC_TIME, nullptr)} << "\n";
29}
Let’s try with LC_ALL=de_DE ./l8n
without any opt-in to locale dependence:
0$ clang++ -o l8n l8n.cpp
1$ LC_ALL=de_DE ./l8n
2C locale at beginning of main: C
3C++ locale at beginning of main: C
4
5std::ostringstream oss; oss << 1000000; oss.str() equals 1000000
6std::to_string(1.23) equals 1.230000
7
8C locale at end of main: C
9C++ locale at end of main: C
10
11setlocale(LC_COLLATE, nullptr) (read-only operation) = C
12setlocale(LC_CTYPE, nullptr) (read-only operation) = C
13setlocale(LC_MESSAGES, nullptr) (read-only operation) = C
14setlocale(LC_MONETARY, nullptr) (read-only operation) = C
15setlocale(LC_NUMERIC, nullptr) (read-only operation) = C
16setlocale(LC_TIME, nullptr) (read-only operation) = C
Let’s try with LC_NUMERIC=de_DE ./l8n
without any opt-in to locale dependence:
0$ clang++ -o l8n l8n.cpp
1$ LC_NUMERIC=de_DE ./l8n
2C locale at beginning of main: C
3C++ locale at beginning of main: C
4
5std::ostringstream oss; oss << 1000000; oss.str() equals 1000000
6std::to_string(1.23) equals 1.230000
7
8C locale at end of main: C
9C++ locale at end of main: C
10
11setlocale(LC_COLLATE, nullptr) (read-only operation) = C
12setlocale(LC_CTYPE, nullptr) (read-only operation) = C
13setlocale(LC_MESSAGES, nullptr) (read-only operation) = C
14setlocale(LC_MONETARY, nullptr) (read-only operation) = C
15setlocale(LC_NUMERIC, nullptr) (read-only operation) = C
16setlocale(LC_TIME, nullptr) (read-only operation) = C
Let’s try LC_ALL=de_DE ./l8n
with opt-in to locale dependence using set_locale
:
0$ clang++ -DOPT_IN_TO_LOCALIZATION_USING_SET_LOCALE -o l8n l8n.cpp
1$ LC_ALL=de_DE ./l8n
2C locale at beginning of main: C
3C++ locale at beginning of main: C
4
5std::ostringstream oss; oss << 1000000; oss.str() equals 1000000
6std::to_string(1.23) equals 1,230000
7
8C locale at end of main: de_DE
9C++ locale at end of main: C
10
11setlocale(LC_COLLATE, nullptr) (read-only operation) = de_DE
12setlocale(LC_CTYPE, nullptr) (read-only operation) = de_DE
13setlocale(LC_MESSAGES, nullptr) (read-only operation) = de_DE
14setlocale(LC_MONETARY, nullptr) (read-only operation) = de_DE
15setlocale(LC_NUMERIC, nullptr) (read-only operation) = de_DE
16setlocale(LC_TIME, nullptr) (read-only operation) = de_DE
Let’s try LC_ALL=de_DE ./l8n
with opt-in to locale dependence using std::locale::global
:
0$ clang++ -DOPT_IN_TO_LOCALIZATION_USING_STD_LOCALE_GLOBAL -o l8n l8n.cpp
1$ LC_ALL=de_DE ./l8n
2C locale at beginning of main: C
3C++ locale at beginning of main: C
4
5std::ostringstream oss; oss << 1000000; oss.str() equals 1.000.000
6std::to_string(1.23) equals 1,230000
7
8C locale at end of main: de_DE
9C++ locale at end of main: de_DE
10
11setlocale(LC_COLLATE, nullptr) (read-only operation) = de_DE
12setlocale(LC_CTYPE, nullptr) (read-only operation) = de_DE
13setlocale(LC_MESSAGES, nullptr) (read-only operation) = de_DE
14setlocale(LC_MONETARY, nullptr) (read-only operation) = de_DE
15setlocale(LC_NUMERIC, nullptr) (read-only operation) = de_DE
16setlocale(LC_TIME, nullptr) (read-only operation) = de_DE