-
Notifications
You must be signed in to change notification settings - Fork 461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Silent failure of locale dependent functions #795
Comments
In some places the usage of jsbsim/src/input_output/FGXMLElement.cpp Lines 294 to 302 in 9cb22d7
But in some other places, the invalid number errors are silently ignored by JSBSim: jsbsim/src/models/flight_control/FGSwitch.cpp Lines 90 to 94 in 9cb22d7
And in some other places, values are read without checking if the syntax is legal: jsbsim/src/math/FGFunction.cpp Lines 601 to 606 in 9cb22d7
Finally, there is also the case where the data is evaluated differently depending on whether or not JSBSim is interpreting the input data as a number: jsbsim/src/models/propulsion/FGTurbine.cpp Lines 488 to 494 in 9cb22d7
|
In addition to adding new error messages, I am wondering if we should force the locale to "C" before calling Something like: char* current_locale = setlocale(LC_NUMERIC, "C");
double value = atof(number_string);
setlocale(LC_NUMERIC, current_locale); I'd guess the performance impact is not a problem since these functions are called only once at the initialization of JSBSim while parsing the XML files of the flight model. Opinions ? |
Yep, I guess one option is as you say to change the locale and change it back whenever we call double atof_locale_c(const char* string)
{
char* current_locale = setlocale(LC_NUMERIC, "C");
double value = atof(number_string);
setlocale(LC_NUMERIC, current_locale);
return value;
} However when looking up some info on For Visual C++ it's https://www.codecogs.com/library/computing/c/stdlib.h/atof.php?alias=atof_l Using this we could create a numeric C locale once and replace all our |
Oh, something else I noticed when looking at JSBSim's |
Yes and we'd need to check
Well, I'd say the function On my side, I have also found https://cplusplus.com/reference/cstdlib/strtod/ The result of the function |
That's a good point: the correct usage should be |
There is also a question that remains open: is |
In terms of However I noticed at the time that there is also a The MSVC CRT has them with an underscore which typically means it's not part of the C/C++ standard, but a number of Linux references have the Switching the global locale each time before and after calling Actually it's looks like at least with MSVC there is a
|
At least, there may be others, |
The other option I came across for converting text to floating point values is the C++ function https://en.cppreference.com/w/cpp/utility/from_chars Although it's only included from the Basically it's locale independent, basically assumes/applies the C locale standard for parsing floating point numbers.
|
Talking of the potential set of functions that are locale specific I came across this list when looking up Also note the comment about undefined behavior with multiple threads. https://en.cppreference.com/w/cpp/locale/setlocale
|
Oh, right. That is indeed a problem.
JSBSim is using C++14 Lines 6 to 7 in 36dda4f
FlightGear has already switched to C++17 so we could switch as well. The only drawback I can think of is that we might need to drop the support of older versions of Python (3.7 ?) and older platforms for Linux wheels. Regarding Python 3.7, this is not much of a problem since according to the PEP 517, the Python foundation will drop support of Python 3.7 in June 2023.
Good, that might be our way out. |
Well, unfortunately
I have implemented a POC but it fails on all compilers but Windows/VS2019 and Ubuntu 22.04/g++ 11.3. |
Hmm, not very promising, it appears we may need to wait a couple more years to be able to use I wonder how well supported |
I just submitted a PR #799 that uses |
What about this variation? Create the required locale once rather than creating and freeing it on every call to Also struct CNumericLocale
{
CNumericLocale()
{
#ifdef _WIN32
Locale = _create_locale(LC_NUMERIC, "C");
#else
Locale = newlocale(LC_NUMERIC_MASK, "C", 0);
#endif
}
~CNumericLocale()
{
#ifdef _WIN32
_free_locale(Locale);
#else
freelocale(Locale);
#endif
}
#ifdef _WIN32
_locale_t Locale;
#else
locale_t Locale;
#endif
};
double atof_locale_c(const std::string& input)
{
static CNumericLocale numeric_c;
const char* first = input.c_str();
// Skip leading whitespaces
while (isspace(*first)) ++first;
//Ignoring the leading '+' sign
if (*first == '+') ++first;
#ifdef _WIN32
double value = _strtod_l(first, nullptr, numeric_c.Locale);
#else
double value = strtod_l(first, nullptr, numeric_c.Locale);
#endif
// Error management
std::stringstream s;
if (fabs(value) == HUGE_VAL && errno == ERANGE)
s << "This number is too large: " << input;
else if (fabs(value) == 0 && errno == EINVAL)
s << "Expecting numeric attribute value, but got: " << input;
else
return value;
std::cerr << s.str() << std::endl;
throw JSBSim::BaseException(s.str());
} |
Or with the odd #ifdef _WIN32
typedef _locale_t locale_t;
#define freelocale _free_locale
#define strtod_l _strtod_l
#endif
struct CNumericLocale
{
CNumericLocale()
{
#ifdef _WIN32
Locale = _create_locale(LC_NUMERIC, "C");
#else
Locale = newlocale(LC_NUMERIC_MASK, "C", 0);
#endif
}
~CNumericLocale()
{
freelocale(Locale);
}
locale_t Locale;
};
double atof_locale_c(const std::string& input)
{
static CNumericLocale numeric_c;
const char* first = input.c_str();
// Skip leading whitespaces
while (isspace(*first)) ++first;
//Ignoring the leading '+' sign
if (*first == '+') ++first;
double value = strtod_l(first, nullptr, numeric_c.Locale);
// Error management
std::stringstream s;
if (fabs(value) == HUGE_VAL && errno == ERANGE)
s << "This number is too large: " << input;
else if (fabs(value) == 0 && errno == EINVAL)
s << "Expecting numeric attribute value, but got: " << input;
else
return value;
std::cerr << s.str() << std::endl;
throw JSBSim::BaseException(s.str());
} |
I like your idea of the However I am not so enthusiastic about the usage of a static variable as we are having the issue #666 opened about that very topic. I'm not sure why we would bother avoiding to create/release a |
Well, while investigating how to use |
In your implementation you call other c-runtime functions like |
I just committed an update to the PR #799 per your suggestions. Note that I did not make the variable
Right. So I removed the copy of However
|
Nope, I haven't done any performance testing to compare. Rather more along the lines of if we only require a single instance for the duration of the program why create and free one for every call to But the issue with static variables and multiple threads can be an issue as we noticed with issue 666. Doing a bit more reading, C++11 makes static local variable initialization thread-safe.
Testing this out with #include <iostream>
#include <thread>
#include <locale>
using namespace std;
struct A
{
A()
{
cout << "A Ctor " << this_thread::get_id() << endl;
Locale = _create_locale(LC_NUMERIC, "C");
}
~A()
{
cout << "A Dtor " << this_thread::get_id() << endl;
_free_locale(Locale);
}
_locale_t Locale;
};
double dummy_atof(const string& data)
{
static A local;
cout << "dummy_atof start " << this_thread::get_id() << endl;
return atof(data.c_str());
}
void threadfunc()
{
cout << "threadfunc start " << this_thread::get_id() << endl;
for (int i = 0; i < 3; i++)
{
dummy_atof("23.45");
this_thread::sleep_for(chrono::milliseconds(rand() % 10));
}
}
int main()
{
for (int i = 0; i < 10; i++)
{
new thread(threadfunc);
}
this_thread::sleep_for(chrono::milliseconds(200));
return 0;
} With the following example output. threadfunc start threadfunc start threadfunc start 60948
threadfunc start 53724
threadfunc start 94180
threadfunc start 12316
A Ctor 60948
threadfunc start 45784
dummy_atof start 45784
dummy_atof start 60948dummy_atof start 12316
threadfunc start 93728
dummy_atof start dummy_atof start 53724
dummy_atof start 94180
threadfunc start 8408093728
threadfunc start 52908
dummy_atof start 45784
46868
dummy_atof start 52908
dummy_atof start dummy_atof start 46868
7832884080
dummy_atof start 78328
dummy_atof start dummy_atof start dummy_atof start dummy_atof start 52908457848408094180
dummy_atof start 78328
dummy_atof start 53724
dummy_atof start 46868
dummy_atof start 12316
dummy_atof start 60948
dummy_atof start 93728
dummy_atof start 84080
dummy_atof start 94180
dummy_atof start 60948
dummy_atof start 93728
dummy_atof start 78328
dummy_atof start 52908
dummy_atof start 12316
dummy_atof start 53724
dummy_atof start 46868
A Dtor 84296
C:\source\temp\ThreadsStatics\x64\Debug\ThreadsStatics.exe (process 85848) exited with code 0. So the ctor is called once and the destructor is called once during program shutdown. Now in issue 666 things are a bit different in that the user's program created multiple threads, each creating an instance of JSBSim, and each instance had a reference to the static Messages class. So now you had multiple threads calling methods of the Message class without any thread synchronization. In this case the ctor gets called once and it's the only method that assigns anything to the single member |
Yep, I noticed that was one possibility, although I thought at the time what are the chances of reading a 0 from the FDM XML file and some previous CRT function had failed with |
So I couldn't resist a micro-benchmark 😉 void performancetest1()
{
auto start = chrono::steady_clock::now();
for (int i = 0; i < 100000; i++)
{
auto Locale = _create_locale(LC_NUMERIC, "C");
double val = _strtod_l("235.8974", nullptr, Locale);
_free_locale(Locale);
}
auto end = chrono::steady_clock::now();
cout << "Elapsed time in milliseconds: "
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
}
void performancetest2()
{
auto Locale = _create_locale(LC_NUMERIC, "C");
auto start = chrono::steady_clock::now();
for (int i = 0; i < 100000; i++)
{
double val = _strtod_l("235.8974", nullptr, Locale);
}
auto end = chrono::steady_clock::now();
_free_locale(Locale);
cout << "Elapsed time in milliseconds: "
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
}
int main()
{
for (int i = 0; i < 10; i++)
{
performancetest1();
performancetest2();
cout << endl;
}
return 0;
} Results for MSVC release build. Elapsed time in milliseconds: 68 ms
Elapsed time in milliseconds: 11 ms
Elapsed time in milliseconds: 96 ms
Elapsed time in milliseconds: 10 ms
Elapsed time in milliseconds: 83 ms
Elapsed time in milliseconds: 15 ms
Elapsed time in milliseconds: 73 ms
Elapsed time in milliseconds: 9 ms
Elapsed time in milliseconds: 67 ms
Elapsed time in milliseconds: 19 ms
Elapsed time in milliseconds: 73 ms
Elapsed time in milliseconds: 9 ms
Elapsed time in milliseconds: 71 ms
Elapsed time in milliseconds: 12 ms
Elapsed time in milliseconds: 85 ms
Elapsed time in milliseconds: 14 ms
Elapsed time in milliseconds: 76 ms
Elapsed time in milliseconds: 11 ms
Elapsed time in milliseconds: 70 ms
Elapsed time in milliseconds: 10 ms
C:\source\temp\ThreadsStatics\x64\Release\ThreadsStatics.exe (process 63112) exited with code 0. So on average an extra How many |
I ran your test on my PC/Win11 and got a ratio of 1:6 as well (but my PC seems twice as slower than yours 😉)
|
So are we ready to commit the last PR of 2022 ? 🥳 |
Go for it! |
Fixes JSBSim-Team#795 Co-authored-by: Sean McLeod <[email protected]>
Fixes JSBSim-Team#795 Co-authored-by: Sean McLeod <[email protected]>
I'm submitting a ...
Describe the issue
As reported by @vranki in the discussion #773, JSBSim can fail reading data if the locale is not set to "C". Functions such as
atof()
may expect the decimal separator to be a comma,
instead of a period.
depending on the locale setting.What is the current behavior?
When the separator does not match the locale setting, data are incorrectly parsed and no error is reported by JSBSim resulting in an unexpected behavior of the flight model.
What is the expected behavior?
JSBSim should report an error when encountering an incorrect syntax while parsing a number.
The text was updated successfully, but these errors were encountered: