Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions Modules/Core/Common/include/itkNumericLocale.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*=========================================================================
*
* Copyright NumFOCUS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*=========================================================================*/
#ifndef itkNumericLocale_h
#define itkNumericLocale_h

#include "itkMacro.h"
#include "ITKCommonExport.h"

#include <locale.h>
#include <cstring>
#include <cstdlib>

// Platform-specific includes for thread-safe locale handling
#if defined(_WIN32)
# include <windows.h>
#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including <windows.h> is unnecessary for this implementation. The _configthreadlocale, _strdup, and setlocale functions are provided by the C runtime library headers (<locale.h>, <stdlib.h>), not by windows.h. Including windows.h adds significant compilation overhead and can cause namespace pollution. Consider removing this include unless there's a specific reason for it.

Suggested change
#if defined(_WIN32)
# include <windows.h>
#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)
#if defined(__APPLE__) || defined(__linux__) || defined(__unix__)

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove all system dependent include in the header. Add a forward declaration for a structure for a pointer to implementation or private implementation.

// POSIX systems provide newlocale/uselocale in locale.h (C11) or xlocale.h (older)
# if defined(__APPLE__)
# include <xlocale.h>
# elif defined(__GLIBC__)
// glibc provides locale_t in locale.h
# else
// Try xlocale.h for other systems, fall back to locale.h
# if __has_include(<xlocale.h>)
# include <xlocale.h>
# endif
# endif
#endif

namespace itk
{

/** \class NumericLocale
* \brief RAII class for thread-safe temporary setting of LC_NUMERIC locale to "C".
*
* This class provides a thread-safe mechanism to temporarily set the LC_NUMERIC
* locale to "C" for locale-independent parsing and formatting of floating-point
* numbers. The original locale is automatically restored when the object goes
* out of scope.
*
* This is particularly useful when parsing file formats that use dot as decimal
* separator (like NRRD, VTK, etc.) regardless of the system locale setting.
*
* Thread safety:
* - On POSIX systems (Linux, macOS, BSD): Uses thread-local locale via uselocale()
* - On Windows: Uses thread-local locale via _configthreadlocale()
* - Fallback: Uses global setlocale() with mutex protection (not fully thread-safe
* for concurrent I/O operations, but prevents corruption of locale state)
*
* Example usage:
* \code
* {
* NumericLocale cLocale;
* // Parse file with dot decimal separator
* double value = std::strtod("3.14159", nullptr);
* // Locale automatically restored here
* }
* \endcode
*
* \ingroup ITKCommon
*/
class ITKCommon_EXPORT NumericLocale
{
public:
/** Constructor: Saves current LC_NUMERIC locale and sets it to "C" */
NumericLocale();

/** Destructor: Restores the original LC_NUMERIC locale */
~NumericLocale();

// Delete copy and move operations
NumericLocale(const NumericLocale &) = delete;
NumericLocale &
operator=(const NumericLocale &) = delete;
NumericLocale(NumericLocale &&) = delete;
NumericLocale &
operator=(NumericLocale &&) = delete;

private:
#if defined(_WIN32)
// Windows: store previous thread-specific locale setting
int m_PreviousThreadLocaleSetting{ -1 };
char * m_SavedLocale{ nullptr };
#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)
// POSIX: store previous thread-local locale
locale_t m_PreviousLocale{ nullptr };
locale_t m_CLocale{ nullptr };
#else
// Fallback: global locale (protected by mutex in implementation)
char * m_SavedLocale{ nullptr };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the system dependent code. Use a C++ smart point to an implementation structure.

#endif
};

} // end namespace itk

#endif // itkNumericLocale_h
1 change: 1 addition & 0 deletions Modules/Core/Common/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ set(
itkLogger.cxx
itkLogOutput.cxx
itkLoggerOutput.cxx
itkNumericLocale.cxx
itkProgressAccumulator.cxx
itkTotalProgressReporter.cxx
itkNumericTraits.cxx
Expand Down
131 changes: 131 additions & 0 deletions Modules/Core/Common/src/itkNumericLocale.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*=========================================================================
*
* Copyright NumFOCUS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*=========================================================================*/

#include "itkNumericLocale.h"

#include <mutex>

namespace itk
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of assuming the thread-specific locale method are an available based on preprocessor defined add CMake try_compile to detect the method. Follow the existing pattern in Modules/Core/Common/CMakeLists.txt, add the test source code to the same CMake directory, and add a cmakedefine to itkConfigurePrivate.h.in.

If neither method is available produce a CMake warning, thread-safe locales are not supported and that the locale will need to be managed and in the application if needed.

{

#if defined(_WIN32)

// Windows implementation using thread-specific locale
NumericLocale::NumericLocale()
{
// Enable thread-specific locale for this thread
m_PreviousThreadLocaleSetting = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);

// Save current LC_NUMERIC locale
const char * currentLocale = setlocale(LC_NUMERIC, nullptr);
if (currentLocale)
{
m_SavedLocale = _strdup(currentLocale);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _strdup function can return nullptr on allocation failure, but this case is not checked. If allocation fails, m_SavedLocale will be nullptr, which is handled safely in the destructor. However, it would be more robust to handle this case explicitly or document the assumption that allocation failure is acceptable here (since the locale will default to "C" and not be restored).

Copilot uses AI. Check for mistakes.
}

// Set to C locale for parsing
setlocale(LC_NUMERIC, "C");
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If setlocale() on line 42 throws an exception (which is unlikely but theoretically possible), the thread-local locale setting enabled on line 32 may not be properly restored. Consider adding exception handling or documenting that setlocale() is assumed not to throw. However, in practice, setlocale() is a C function that typically doesn't throw, so this is a low-risk issue.

Copilot uses AI. Check for mistakes.
}

NumericLocale::~NumericLocale()
{
// Restore original locale
if (m_SavedLocale)
{
setlocale(LC_NUMERIC, m_SavedLocale);
free(m_SavedLocale);
}

// Restore previous thread-specific locale setting
if (m_PreviousThreadLocaleSetting != -1)
{
_configthreadlocale(m_PreviousThreadLocaleSetting);
}
}

#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)

// POSIX implementation using thread-local locale (uselocale/newlocale)
NumericLocale::NumericLocale()
{
// Create a new C locale
m_CLocale = newlocale(LC_NUMERIC_MASK, "C", nullptr);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newlocale function can return nullptr on failure (e.g., if the "C" locale is not available or memory allocation fails). This should be checked, and if it fails, the code should either fall back to a safe behavior or document why failure is acceptable. Currently, if newlocale fails, m_CLocale will be nullptr, and uselocale will not be called (line 69 check), which means the locale won't be changed at all - this might be acceptable as a silent fallback, but should be documented or logged.

Copilot uses AI. Check for mistakes.

if (m_CLocale)
{
// Set the C locale for this thread and save the previous locale
m_PreviousLocale = uselocale(m_CLocale);
}
}

NumericLocale::~NumericLocale()
{
// Restore the previous locale for this thread
if (m_PreviousLocale)
{
uselocale(m_PreviousLocale);
}

// Free the C locale
if (m_CLocale)
{
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for m_PreviousLocale using if (m_PreviousLocale) may not be correct for all cases. According to POSIX specification, uselocale() can return LC_GLOBAL_LOCALE (typically defined as ((locale_t) -1)), which is a valid return value indicating that the thread was using the global locale. This value is non-null, so the check would pass. However, according to POSIX, uselocale() should always restore the previous locale setting, even if it was LC_GLOBAL_LOCALE. The current implementation should work correctly, but the null check suggests defensive programming - it might be clearer to unconditionally call uselocale(m_PreviousLocale) since uselocale always returns a valid locale_t on success.

Suggested change
// Restore the previous locale for this thread
if (m_PreviousLocale)
{
uselocale(m_PreviousLocale);
}
// Free the C locale
if (m_CLocale)
{
// If we created and installed a C locale for this thread,
// restore the previous locale and free the C locale.
if (m_CLocale)
{
uselocale(m_PreviousLocale);

Copilot uses AI. Check for mistakes.
freelocale(m_CLocale);
}
}

#else

// Fallback implementation using global locale with mutex protection
// This is not ideal but provides some protection against concurrent access
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the fall back implementation to only produce an ITK warning I the locale does not match the expected location. Do not use a mutex. Do not change the locale..

namespace
{
std::mutex localeMutex;
}

NumericLocale::NumericLocale()
{
// Lock mutex to protect global locale state
localeMutex.lock();

// Save current LC_NUMERIC locale
const char * currentLocale = setlocale(LC_NUMERIC, nullptr);
if (currentLocale)
{
m_SavedLocale = strdup(currentLocale);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strdup function can return nullptr on allocation failure, but this case is not checked. If allocation fails, m_SavedLocale will be nullptr, which is handled safely in the destructor. However, it would be more robust to handle this case explicitly or document the assumption that allocation failure is acceptable here (since the locale will default to "C" and not be restored).

Suggested change
m_SavedLocale = strdup(currentLocale);
char * duplicatedLocale = strdup(currentLocale);
if (duplicatedLocale != nullptr)
{
m_SavedLocale = duplicatedLocale;
}
// If duplication fails, m_SavedLocale remains null and the locale
// will not be restored in the destructor; the "C" locale stays active.

Copilot uses AI. Check for mistakes.
}

// Set to C locale for parsing
setlocale(LC_NUMERIC, "C");
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mutex is locked in the constructor but will remain locked if setlocale() on line 113 throws an exception. This creates a potential deadlock scenario. Consider using std::lock_guard or std::unique_lock for RAII-based mutex management to ensure the mutex is always unlocked even if an exception occurs.

Suggested change
setlocale(LC_NUMERIC, "C");
try
{
setlocale(LC_NUMERIC, "C");
}
catch (...)
{
// Ensure mutex is unlocked if setlocale throws, to avoid deadlock
localeMutex.unlock();
throw;
}

Copilot uses AI. Check for mistakes.
}

NumericLocale::~NumericLocale()
{
// Restore original locale
if (m_SavedLocale)
{
setlocale(LC_NUMERIC, m_SavedLocale);
free(m_SavedLocale);
}

// Unlock mutex
localeMutex.unlock();
}

#endif

} // end namespace itk
17 changes: 17 additions & 0 deletions Modules/IO/NRRD/src/itkNrrdImageIO.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "itkMetaDataObject.h"
#include "itkIOCommon.h"
#include "itkFloatingPointExceptions.h"
#include "itkNumericLocale.h"

#include <sstream>

Expand Down Expand Up @@ -395,6 +396,12 @@ NrrdImageIO::ReadImageInformation()
FloatingPointExceptions::Disable();
}

// Set LC_NUMERIC to "C" locale to ensure locale-independent parsing
// of floating-point values in NRRD headers (spacing, origin, direction, etc.).
// This prevents issues in locales that use comma as decimal separator.
// Using thread-safe NumericLocale from ITKCommon.
NumericLocale cLocale;

// this is the mechanism by which we tell nrrdLoad to read
// just the header, and none of the data
nrrdIoStateSet(nio, nrrdIoStateSkipData, 1);
Expand Down Expand Up @@ -923,6 +930,11 @@ NrrdImageIO::Read(void * buffer)
FloatingPointExceptions::Disable();
#endif

// Set LC_NUMERIC to "C" locale to ensure locale-independent parsing
// of floating-point values in NRRD headers.
// Using thread-safe NumericLocale from ITKCommon.
NumericLocale cLocale;

// Read in the nrrd. Yes, this means that the header is being read
// twice: once by NrrdImageIO::ReadImageInformation, and once here
if (nrrdLoad(nrrd, this->GetFileName(), nullptr) != 0)
Expand Down Expand Up @@ -1338,6 +1350,11 @@ NrrdImageIO::Write(const void * buffer)
break;
}

// Set LC_NUMERIC to "C" locale to ensure locale-independent formatting
// of floating-point values when writing NRRD headers.
// Using thread-safe NumericLocale from ITKCommon.
NumericLocale cLocale;

// Write the nrrd to file.
if (nrrdSave(this->GetFileName(), nrrd, nio))
{
Expand Down
9 changes: 9 additions & 0 deletions Modules/IO/NRRD/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ set(
itkNrrdVectorImageUnitLabelReadTest.cxx
itkNrrd5dVectorImageReadWriteTest.cxx
itkNrrdMetaDataTest.cxx
itkNrrdLocaleTest.cxx
)

# For itkNrrdImageIOTest.h.
Expand Down Expand Up @@ -356,3 +357,11 @@ itk_add_test(
itkNrrdMetaDataTest
${ITK_TEST_OUTPUT_DIR}
)

itk_add_test(
NAME itkNrrdLocaleTest
COMMAND
ITKIONRRDTestDriver
itkNrrdLocaleTest
${ITK_TEST_OUTPUT_DIR}
)
Loading
Loading