v4 adds error handling, in case newlocale("C") fails.  I created CF
entry #5166 for this.
From 3543cc04b0d66c7f83bdccbb247eedd93cdea4af Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.mu...@gmail.com>
Date: Sat, 10 Aug 2024 11:01:21 +1200
Subject: [PATCH v4] Improve locale thread safety of ECPG.

Remove the use of setlocale() as a fallback strategy on systems that
don't have uselocale(), where ECPG tries to control LC_NUMERIC
formatting on input and output of floating point numbers.

Instead, use the C locale explicitly.  Provide the name PG_C_LOCALE for
this purpose.  On a couple of systems this maps to a system-provided
LC_C_LOCALE (a non-standard extension to POSIX), and otherwise we
provide a thread-safe singleton constructor that allocates a locale_t
for (LC_ALL, "C") for the lifetime of the process.

1.  Use strtod_l(..., PG_C_LOCALE) for parsing floats, and supply an
implementation of that common but non-standard extension with
standard uselocale() + strtod() if it is missing.

2.  Inside our own snprintf.c, in front-end code only, use non-standard
snprintf_l() where we punt floating point numbers to the system
snprintf(), or wrap it in uselocale() if not available.

Since the non-standard _l() functions require <xlocale.h> on *BSD/macOS
systems, simplify the configure probes: instead of XXX_IN_XLOCALE_H for
several features XXX, let's just include <xlocale.h> if HAVE_XLOCALE_H.
The reason for the extra complication was apparently that some old glibc
systems also had an <xlocale.h>, and you weren't supposed to include it
directly, but it's gone now (as far as I can tell it was harmless to do
so anyway).

Discussion: https://postgr.es/m/CWZBBRR6YA8D.8EHMDRGLCKCD%40neon.tech
---
 config/c-library.m4                          |  55 ---------
 configure                                    | 115 +------------------
 configure.ac                                 |   6 +-
 meson.build                                  |  37 +-----
 src/include/pg_config.h.in                   |  15 +--
 src/include/port.h                           |  42 +++++++
 src/include/utils/pg_locale.h                |   2 +-
 src/interfaces/ecpg/ecpglib/connect.c        |  39 +------
 src/interfaces/ecpg/ecpglib/data.c           |   2 +-
 src/interfaces/ecpg/ecpglib/descriptor.c     |  37 ------
 src/interfaces/ecpg/ecpglib/ecpglib_extern.h |  15 ---
 src/interfaces/ecpg/ecpglib/execute.c        |  55 ---------
 src/interfaces/ecpg/pgtypeslib/dt_common.c   |   6 +-
 src/interfaces/ecpg/pgtypeslib/interval.c    |   4 +-
 src/interfaces/ecpg/pgtypeslib/numeric.c     |   2 +-
 src/port/Makefile                            |   1 +
 src/port/locale.c                            |  78 +++++++++++++
 src/port/meson.build                         |   1 +
 src/port/snprintf.c                          |  53 +++++++++
 19 files changed, 196 insertions(+), 369 deletions(-)
 create mode 100644 src/port/locale.c

diff --git a/config/c-library.m4 b/config/c-library.m4
index aa8223d2ef0..421bc612b27 100644
--- a/config/c-library.m4
+++ b/config/c-library.m4
@@ -81,58 +81,3 @@ AC_DEFUN([PGAC_STRUCT_SOCKADDR_SA_LEN],
 [#include <sys/types.h>
 #include <sys/socket.h>
 ])])# PGAC_STRUCT_SOCKADDR_MEMBERS
-
-
-# PGAC_TYPE_LOCALE_T
-# ------------------
-# Check for the locale_t type and find the right header file.  macOS
-# needs xlocale.h; standard is locale.h, but glibc <= 2.25 also had an
-# xlocale.h file that we should not use, so we check the standard
-# header first.
-AC_DEFUN([PGAC_TYPE_LOCALE_T],
-[AC_CACHE_CHECK([for locale_t], pgac_cv_type_locale_t,
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <locale.h>
-locale_t x;],
-[])],
-[pgac_cv_type_locale_t=yes],
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <xlocale.h>
-locale_t x;],
-[])],
-[pgac_cv_type_locale_t='yes (in xlocale.h)'],
-[pgac_cv_type_locale_t=no])])])
-if test "$pgac_cv_type_locale_t" = 'yes (in xlocale.h)'; then
-  AC_DEFINE(LOCALE_T_IN_XLOCALE, 1,
-            [Define to 1 if `locale_t' requires <xlocale.h>.])
-fi])# PGAC_TYPE_LOCALE_T
-
-
-# PGAC_FUNC_WCSTOMBS_L
-# --------------------
-# Try to find a declaration for wcstombs_l().  It might be in stdlib.h
-# (following the POSIX requirement for wcstombs()), or in locale.h, or in
-# xlocale.h.  If it's in the latter, define WCSTOMBS_L_IN_XLOCALE.
-#
-AC_DEFUN([PGAC_FUNC_WCSTOMBS_L],
-[AC_CACHE_CHECK([for wcstombs_l declaration], pgac_cv_func_wcstombs_l,
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <stdlib.h>
-#include <locale.h>],
-[#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif])],
-[pgac_cv_func_wcstombs_l='yes'],
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <stdlib.h>
-#include <locale.h>
-#include <xlocale.h>],
-[#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif])],
-[pgac_cv_func_wcstombs_l='yes (in xlocale.h)'],
-[pgac_cv_func_wcstombs_l='no'])])])
-if test "$pgac_cv_func_wcstombs_l" = 'yes (in xlocale.h)'; then
-  AC_DEFINE(WCSTOMBS_L_IN_XLOCALE, 1,
-            [Define to 1 if `wcstombs_l' requires <xlocale.h>.])
-fi])# PGAC_FUNC_WCSTOMBS_L
diff --git a/configure b/configure
index 4f3aa447566..298492ec251 100755
--- a/configure
+++ b/configure
@@ -14635,55 +14635,6 @@ _ACEOF
 fi
 
 
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for locale_t" >&5
-$as_echo_n "checking for locale_t... " >&6; }
-if ${pgac_cv_type_locale_t+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <locale.h>
-locale_t x;
-int
-main ()
-{
-
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_type_locale_t=yes
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <xlocale.h>
-locale_t x;
-int
-main ()
-{
-
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_type_locale_t='yes (in xlocale.h)'
-else
-  pgac_cv_type_locale_t=no
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_type_locale_t" >&5
-$as_echo "$pgac_cv_type_locale_t" >&6; }
-if test "$pgac_cv_type_locale_t" = 'yes (in xlocale.h)'; then
-
-$as_echo "#define LOCALE_T_IN_XLOCALE 1" >>confdefs.h
-
-fi
-
 # MSVC doesn't cope well with defining restrict to __restrict, the
 # spelling it understands, because it conflicts with
 # __declspec(restrict). Therefore we define pg_restrict to the
@@ -15170,59 +15121,6 @@ if test x"$pgac_cv_var_int_timezone" = xyes ; then
 
 $as_echo "#define HAVE_INT_TIMEZONE 1" >>confdefs.h
 
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for wcstombs_l declaration" >&5
-$as_echo_n "checking for wcstombs_l declaration... " >&6; }
-if ${pgac_cv_func_wcstombs_l+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <stdlib.h>
-#include <locale.h>
-int
-main ()
-{
-#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_func_wcstombs_l='yes'
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <stdlib.h>
-#include <locale.h>
-#include <xlocale.h>
-int
-main ()
-{
-#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_func_wcstombs_l='yes (in xlocale.h)'
-else
-  pgac_cv_func_wcstombs_l='no'
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_func_wcstombs_l" >&5
-$as_echo "$pgac_cv_func_wcstombs_l" >&6; }
-if test "$pgac_cv_func_wcstombs_l" = 'yes (in xlocale.h)'; then
-
-$as_echo "#define WCSTOMBS_L_IN_XLOCALE 1" >>confdefs.h
-
 fi
 
 # Some versions of libedit contain strlcpy(), setproctitle(), and other
@@ -15232,7 +15130,7 @@ fi
 LIBS_including_readline="$LIBS"
 LIBS=`echo "$LIBS" | sed -e 's/-ledit//g' -e 's/-lreadline//g'`
 
-for ac_func in backtrace_symbols copyfile copy_file_range getifaddrs getpeerucred inet_pton kqueue mbstowcs_l memset_s posix_fallocate ppoll pthread_is_threaded_np setproctitle setproctitle_fast strchrnul strsignal syncfs sync_file_range uselocale wcstombs_l
+for ac_func in backtrace_symbols copyfile copy_file_range getifaddrs getpeerucred inet_pton kqueue mbstowcs_l memset_s posix_fallocate ppoll pthread_is_threaded_np setproctitle setproctitle_fast snprintf_l strtod_l strchrnul strsignal syncfs sync_file_range uselocale wcstombs_l
 do :
   as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
 ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
@@ -16005,17 +15903,6 @@ fi
 
 # Win32 (really MinGW) support
 if test "$PORTNAME" = "win32"; then
-  for ac_func in _configthreadlocale
-do :
-  ac_fn_c_check_func "$LINENO" "_configthreadlocale" "ac_cv_func__configthreadlocale"
-if test "x$ac_cv_func__configthreadlocale" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE__CONFIGTHREADLOCALE 1
-_ACEOF
-
-fi
-done
-
   case " $LIBOBJS " in
   *" dirmod.$ac_objext "* ) ;;
   *) LIBOBJS="$LIBOBJS dirmod.$ac_objext"
diff --git a/configure.ac b/configure.ac
index 049bc014911..302aadd427d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1620,8 +1620,6 @@ PGAC_UNION_SEMUN
 AC_CHECK_TYPES(socklen_t, [], [], [#include <sys/socket.h>])
 PGAC_STRUCT_SOCKADDR_SA_LEN
 
-PGAC_TYPE_LOCALE_T
-
 # MSVC doesn't cope well with defining restrict to __restrict, the
 # spelling it understands, because it conflicts with
 # __declspec(restrict). Therefore we define pg_restrict to the
@@ -1720,7 +1718,6 @@ fi
 ##
 
 PGAC_VAR_INT_TIMEZONE
-PGAC_FUNC_WCSTOMBS_L
 
 # Some versions of libedit contain strlcpy(), setproctitle(), and other
 # symbols that that library has no business exposing to the world.  Pending
@@ -1744,6 +1741,8 @@ AC_CHECK_FUNCS(m4_normalize([
 	pthread_is_threaded_np
 	setproctitle
 	setproctitle_fast
+	snprintf_l
+	strtod_l
 	strchrnul
 	strsignal
 	syncfs
@@ -1833,7 +1832,6 @@ fi
 
 # Win32 (really MinGW) support
 if test "$PORTNAME" = "win32"; then
-  AC_CHECK_FUNCS(_configthreadlocale)
   AC_LIBOBJ(dirmod)
   AC_LIBOBJ(kill)
   AC_LIBOBJ(open)
diff --git a/meson.build b/meson.build
index cc176f11b5d..932334ed634 100644
--- a/meson.build
+++ b/meson.build
@@ -2407,6 +2407,7 @@ header_checks = [
   'sys/ucred.h',
   'termios.h',
   'ucred.h',
+  'xlocale.h',
 ]
 
 foreach header : header_checks
@@ -2550,15 +2551,6 @@ else
   cdata.set('STRERROR_R_INT', false)
 endif
 
-# Find the right header file for the locale_t type.  macOS needs xlocale.h;
-# standard is locale.h, but glibc <= 2.25 also had an xlocale.h file that
-# we should not use so we check the standard header first.  MSVC has a
-# replacement defined in src/include/port/win32_port.h.
-if not cc.has_type('locale_t', prefix: '#include <locale.h>') and \
-   cc.has_type('locale_t', prefix: '#include <xlocale.h>')
-  cdata.set('LOCALE_T_IN_XLOCALE', 1)
-endif
-
 # Check if the C compiler understands typeof or a variant.  Define
 # HAVE_TYPEOF if so, and define 'typeof' to the actual key word.
 foreach kw : ['typeof', '__typeof__', 'decltype']
@@ -2583,30 +2575,6 @@ int main(void)
   endif
 endforeach
 
-
-# Try to find a declaration for wcstombs_l().  It might be in stdlib.h
-# (following the POSIX requirement for wcstombs()), or in locale.h, or in
-# xlocale.h.  If it's in the latter, define WCSTOMBS_L_IN_XLOCALE.
-wcstombs_l_test = '''
-#include <stdlib.h>
-#include <locale.h>
-@0@
-
-void main(void)
-{
-#ifndef wcstombs_l
-    (void) wcstombs_l;
-#endif
-}
-'''
-if (not cc.compiles(wcstombs_l_test.format(''),
-      name: 'wcstombs_l') and
-    cc.compiles(wcstombs_l_test.format('#include <xlocale.h>'),
-      name: 'wcstombs_l in xlocale.h'))
-    cdata.set('WCSTOMBS_L_IN_XLOCALE', 1)
-endif
-
-
 # MSVC doesn't cope well with defining restrict to __restrict, the spelling it
 # understands, because it conflicts with __declspec(restrict). Therefore we
 # define pg_restrict to the appropriate definition, which presumably won't
@@ -2658,7 +2626,6 @@ endif
 # XXX: Might be worth conditioning some checks on the OS, to avoid doing
 # unnecessary checks over and over, particularly on windows.
 func_checks = [
-  ['_configthreadlocale', {'skip': host_system != 'windows'}],
   ['backtrace_symbols', {'dependencies': [execinfo_dep]}],
   ['clock_gettime', {'dependencies': [rt_dep], 'define': false}],
   ['copyfile'],
@@ -2690,6 +2657,8 @@ func_checks = [
   ['shm_open', {'dependencies': [rt_dep], 'define': false}],
   ['shm_unlink', {'dependencies': [rt_dep], 'define': false}],
   ['shmget', {'dependencies': [cygipc_dep], 'define': false}],
+  ['snprintf_l'],
+  ['strtod_l'],
   ['socket', {'dependencies': [socket_dep], 'define': false}],
   ['strchrnul'],
   ['strerror_r', {'dependencies': [thread_dep]}],
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 0e9b108e667..c0586edab13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -376,6 +376,9 @@
 /* Define to 1 if you have the `setproctitle_fast' function. */
 #undef HAVE_SETPROCTITLE_FAST
 
+/* Define to 1 if you have the `snprintf_l' function. */
+#undef HAVE_SNPRINTF_L
+
 /* Define to 1 if the system has the type `socklen_t'. */
 #undef HAVE_SOCKLEN_T
 
@@ -421,6 +424,9 @@
 /* Define to 1 if you have the `strsignal' function. */
 #undef HAVE_STRSIGNAL
 
+/* Define to 1 if you have the `strtod_l' function. */
+#undef HAVE_STRTOD_L
+
 /* Define to 1 if the system has the type `struct option'. */
 #undef HAVE_STRUCT_OPTION
 
@@ -556,9 +562,6 @@
 /* Define to 1 if your compiler understands __builtin_unreachable. */
 #undef HAVE__BUILTIN_UNREACHABLE
 
-/* Define to 1 if you have the `_configthreadlocale' function. */
-#undef HAVE__CONFIGTHREADLOCALE
-
 /* Define to 1 if you have __cpuid. */
 #undef HAVE__CPUID
 
@@ -577,9 +580,6 @@
 /* Define to the appropriate printf length modifier for 64-bit ints. */
 #undef INT64_MODIFIER
 
-/* Define to 1 if `locale_t' requires <xlocale.h>. */
-#undef LOCALE_T_IN_XLOCALE
-
 /* Define as the maximum alignment requirement of any C data type. */
 #undef MAXIMUM_ALIGNOF
 
@@ -766,9 +766,6 @@
 /* Define to 1 to build with ZSTD support. (--with-zstd) */
 #undef USE_ZSTD
 
-/* Define to 1 if `wcstombs_l' requires <xlocale.h>. */
-#undef WCSTOMBS_L_IN_XLOCALE
-
 /* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
    significant byte first (like Motorola and SPARC, unlike Intel). */
 #if defined AC_APPLE_UNIVERSAL_BUILD
diff --git a/src/include/port.h b/src/include/port.h
index c7400052675..8e814700e7f 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -15,6 +15,11 @@
 
 #include <ctype.h>
 
+#include <locale.h>
+#ifdef HAVE_XLOCALE_H
+#include <xlocale.h>
+#endif
+
 /*
  * Windows has enough specialized port stuff that we push most of it off
  * into another file.
@@ -217,6 +222,43 @@ extern int	pg_fprintf(FILE *stream, const char *fmt,...) pg_attribute_printf(2,
 extern int	pg_vprintf(const char *fmt, va_list args) pg_attribute_printf(1, 0);
 extern int	pg_printf(const char *fmt,...) pg_attribute_printf(1, 2);
 
+/* Thread-safe singleton constructor for process-lifetime "C" locale. */
+extern locale_t pg_get_c_locale(void);
+
+/*
+ * A couple of systems offer a pre-defined locale_t value for the "C" locale.
+ * Otherwise fall back to the above function.  Provide a way to check that a
+ * "C" locale has been allocated at a convenient time for error reporting.
+ */
+#ifdef LC_C_LOCALE
+#define PG_C_LOCALE LC_C_LOCALE
+#define pg_ensure_c_locale() true
+#else
+#define PG_C_LOCALE pg_get_c_locale()
+#define pg_ensure_c_locale() (pg_get_c_locale() != (locale_t) 0)
+#endif
+
+#ifndef HAVE_STRTOD_L
+/*
+ * POSIX doesn't define this function, but we can implement it with thread-safe
+ * save-and-restore.  Not all systems have uselocale(), but the ones that don't
+ * have strtod_l().
+ */
+static inline double
+strtod_l(const char *nptr, char **endptr, locale_t loc)
+{
+#ifdef WIN32
+	return _strtod_l(nptr, endptr, loc);
+#else
+	locale_t	save = uselocale(loc);
+	double		result = strtod(nptr, endptr);
+
+	uselocale(save);
+	return result;
+#endif
+}
+#endif
+
 #ifndef WIN32
 /*
  * We add a pg_ prefix as a warning that the Windows implementations have the
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index f41d33975be..9651f3c3ab5 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -12,7 +12,7 @@
 #ifndef _PG_LOCALE_
 #define _PG_LOCALE_
 
-#if defined(LOCALE_T_IN_XLOCALE) || defined(WCSTOMBS_L_IN_XLOCALE)
+#ifdef HAVE_XLOCALE_H
 #include <xlocale.h>
 #endif
 #ifdef USE_ICU
diff --git a/src/interfaces/ecpg/ecpglib/connect.c b/src/interfaces/ecpg/ecpglib/connect.c
index 8afb1f0a26f..f4ba5b722eb 100644
--- a/src/interfaces/ecpg/ecpglib/connect.c
+++ b/src/interfaces/ecpg/ecpglib/connect.c
@@ -10,10 +10,6 @@
 #include "ecpgtype.h"
 #include "sqlca.h"
 
-#ifdef HAVE_USELOCALE
-locale_t	ecpg_clocale = (locale_t) 0;
-#endif
-
 static pthread_mutex_t connections_mutex = PTHREAD_MUTEX_INITIALIZER;
 static pthread_key_t actual_connection_key;
 static pthread_once_t actual_connection_key_once = PTHREAD_ONCE_INIT;
@@ -268,7 +264,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
 	const char **conn_keywords;
 	const char **conn_values;
 
-	if (sqlca == NULL)
+	if (sqlca == NULL || !pg_ensure_c_locale())
 	{
 		ecpg_raise(lineno, ECPG_OUT_OF_MEMORY,
 				   ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
@@ -483,39 +479,6 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
 	/* add connection to our list */
 	pthread_mutex_lock(&connections_mutex);
 
-	/*
-	 * ... but first, make certain we have created ecpg_clocale.  Rely on
-	 * holding connections_mutex to ensure this is done by only one thread.
-	 */
-#ifdef HAVE_USELOCALE
-	if (!ecpg_clocale)
-	{
-		ecpg_clocale = newlocale(LC_NUMERIC_MASK, "C", (locale_t) 0);
-		if (!ecpg_clocale)
-		{
-			pthread_mutex_unlock(&connections_mutex);
-			ecpg_raise(lineno, ECPG_OUT_OF_MEMORY,
-					   ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
-			if (host)
-				ecpg_free(host);
-			if (port)
-				ecpg_free(port);
-			if (options)
-				ecpg_free(options);
-			if (realname)
-				ecpg_free(realname);
-			if (dbname)
-				ecpg_free(dbname);
-			if (conn_keywords)
-				ecpg_free(conn_keywords);
-			if (conn_values)
-				ecpg_free(conn_values);
-			free(this);
-			return false;
-		}
-	}
-#endif
-
 	if (connection_name != NULL)
 		this->name = ecpg_strdup(connection_name, lineno);
 	else
diff --git a/src/interfaces/ecpg/ecpglib/data.c b/src/interfaces/ecpg/ecpglib/data.c
index fa562767585..856f4c9472d 100644
--- a/src/interfaces/ecpg/ecpglib/data.c
+++ b/src/interfaces/ecpg/ecpglib/data.c
@@ -466,7 +466,7 @@ ecpg_get_data(const PGresult *results, int act_tuple, int act_field, int lineno,
 						pval++;
 
 					if (!check_special_value(pval, &dres, &scan_length))
-						dres = strtod(pval, &scan_length);
+						dres = strtod_l(pval, &scan_length, PG_C_LOCALE);
 
 					if (isarray && *scan_length == '"')
 						scan_length++;
diff --git a/src/interfaces/ecpg/ecpglib/descriptor.c b/src/interfaces/ecpg/ecpglib/descriptor.c
index ad279e245c4..855322e037f 100644
--- a/src/interfaces/ecpg/ecpglib/descriptor.c
+++ b/src/interfaces/ecpg/ecpglib/descriptor.c
@@ -475,46 +475,9 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...)
 		memset(&stmt, 0, sizeof stmt);
 		stmt.lineno = lineno;
 
-		/* Make sure we do NOT honor the locale for numeric input */
-		/* since the database gives the standard decimal point */
-		/* (see comments in execute.c) */
-#ifdef HAVE_USELOCALE
-
-		/*
-		 * To get here, the above PQnfields() test must have found nonzero
-		 * fields.  One needs a connection to create such a descriptor.  (EXEC
-		 * SQL SET DESCRIPTOR can populate the descriptor's "items", but it
-		 * can't change the descriptor's PQnfields().)  Any successful
-		 * connection initializes ecpg_clocale.
-		 */
-		Assert(ecpg_clocale);
-		stmt.oldlocale = uselocale(ecpg_clocale);
-#else
-#ifdef HAVE__CONFIGTHREADLOCALE
-		stmt.oldthreadlocale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
-#endif
-		stmt.oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno);
-		setlocale(LC_NUMERIC, "C");
-#endif
-
 		/* desperate try to guess something sensible */
 		stmt.connection = ecpg_get_connection(NULL);
 		ecpg_store_result(ECPGresult, index, &stmt, &data_var);
-
-#ifdef HAVE_USELOCALE
-		if (stmt.oldlocale != (locale_t) 0)
-			uselocale(stmt.oldlocale);
-#else
-		if (stmt.oldlocale)
-		{
-			setlocale(LC_NUMERIC, stmt.oldlocale);
-			ecpg_free(stmt.oldlocale);
-		}
-#ifdef HAVE__CONFIGTHREADLOCALE
-		if (stmt.oldthreadlocale != -1)
-			(void) _configthreadlocale(stmt.oldthreadlocale);
-#endif
-#endif
 	}
 	else if (data_var.ind_type != ECPGt_NO_INDICATOR && data_var.ind_pointer != NULL)
 
diff --git a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
index 01b4309a710..40988d53575 100644
--- a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
+++ b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
@@ -13,9 +13,6 @@
 #ifndef CHAR_BIT
 #include <limits.h>
 #endif
-#ifdef LOCALE_T_IN_XLOCALE
-#include <xlocale.h>
-#endif
 
 enum COMPAT_MODE
 {
@@ -59,10 +56,6 @@ struct ECPGtype_information_cache
 	enum ARRAY_TYPE isarray;
 };
 
-#ifdef HAVE_USELOCALE
-extern locale_t ecpg_clocale;	/* LC_NUMERIC=C */
-#endif
-
 /* structure to store one statement */
 struct statement
 {
@@ -76,14 +69,6 @@ struct statement
 	bool		questionmarks;
 	struct variable *inlist;
 	struct variable *outlist;
-#ifdef HAVE_USELOCALE
-	locale_t	oldlocale;
-#else
-	char	   *oldlocale;
-#ifdef HAVE__CONFIGTHREADLOCALE
-	int			oldthreadlocale;
-#endif
-#endif
 	int			nparams;
 	char	  **paramvalues;
 	int		   *paramlengths;
diff --git a/src/interfaces/ecpg/ecpglib/execute.c b/src/interfaces/ecpg/ecpglib/execute.c
index 04d0b40c537..806d4779fa9 100644
--- a/src/interfaces/ecpg/ecpglib/execute.c
+++ b/src/interfaces/ecpg/ecpglib/execute.c
@@ -101,9 +101,6 @@ free_statement(struct statement *stmt)
 	free_variable(stmt->outlist);
 	ecpg_free(stmt->command);
 	ecpg_free(stmt->name);
-#ifndef HAVE_USELOCALE
-	ecpg_free(stmt->oldlocale);
-#endif
 	ecpg_free(stmt);
 }
 
@@ -1973,40 +1970,6 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator,
 	if (stmt == NULL)
 		return false;
 
-	/*
-	 * Make sure we do NOT honor the locale for numeric input/output since the
-	 * database wants the standard decimal point.  If available, use
-	 * uselocale() for this because it's thread-safe.  Windows doesn't have
-	 * that, but it usually does have _configthreadlocale().  In some versions
-	 * of MinGW, _configthreadlocale() exists but always returns -1 --- so
-	 * treat that situation as if the function doesn't exist.
-	 */
-#ifdef HAVE_USELOCALE
-
-	/*
-	 * Since ecpg_init() succeeded, we have a connection.  Any successful
-	 * connection initializes ecpg_clocale.
-	 */
-	Assert(ecpg_clocale);
-	stmt->oldlocale = uselocale(ecpg_clocale);
-	if (stmt->oldlocale == (locale_t) 0)
-	{
-		ecpg_do_epilogue(stmt);
-		return false;
-	}
-#else
-#ifdef HAVE__CONFIGTHREADLOCALE
-	stmt->oldthreadlocale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
-#endif
-	stmt->oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno);
-	if (stmt->oldlocale == NULL)
-	{
-		ecpg_do_epilogue(stmt);
-		return false;
-	}
-	setlocale(LC_NUMERIC, "C");
-#endif
-
 	/*
 	 * If statement type is ECPGst_prepnormal we are supposed to prepare the
 	 * statement before executing them
@@ -2213,24 +2176,6 @@ ecpg_do_epilogue(struct statement *stmt)
 	if (stmt == NULL)
 		return;
 
-#ifdef HAVE_USELOCALE
-	if (stmt->oldlocale != (locale_t) 0)
-		uselocale(stmt->oldlocale);
-#else
-	if (stmt->oldlocale)
-		setlocale(LC_NUMERIC, stmt->oldlocale);
-#ifdef HAVE__CONFIGTHREADLOCALE
-
-	/*
-	 * This is a bit trickier than it looks: if we failed partway through
-	 * statement initialization, oldthreadlocale could still be 0.  But that's
-	 * okay because a call with 0 is defined to be a no-op.
-	 */
-	if (stmt->oldthreadlocale != -1)
-		(void) _configthreadlocale(stmt->oldthreadlocale);
-#endif
-#endif
-
 	free_statement(stmt);
 }
 
diff --git a/src/interfaces/ecpg/pgtypeslib/dt_common.c b/src/interfaces/ecpg/pgtypeslib/dt_common.c
index ed08088bfe1..92459728bf4 100644
--- a/src/interfaces/ecpg/pgtypeslib/dt_common.c
+++ b/src/interfaces/ecpg/pgtypeslib/dt_common.c
@@ -1216,7 +1216,7 @@ DecodeNumber(int flen, char *str, int fmask,
 			return DecodeNumberField(flen, str, (fmask | DTK_DATE_M),
 									 tmask, tm, fsec, is2digits);
 
-		*fsec = strtod(cp, &cp);
+		*fsec = strtod_l(cp, &cp, PG_C_LOCALE);
 		if (*cp != '\0')
 			return -1;
 	}
@@ -2028,7 +2028,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		frac;
 
-								frac = strtod(cp, &cp);
+								frac = strtod_l(cp, &cp, PG_C_LOCALE);
 								if (*cp != '\0')
 									return -1;
 								*fsec = frac * 1000000;
@@ -2052,7 +2052,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		time;
 
-								time = strtod(cp, &cp);
+								time = strtod_l(cp, &cp, PG_C_LOCALE);
 								if (*cp != '\0')
 									return -1;
 
diff --git a/src/interfaces/ecpg/pgtypeslib/interval.c b/src/interfaces/ecpg/pgtypeslib/interval.c
index 936a6883816..155c6cc7770 100644
--- a/src/interfaces/ecpg/pgtypeslib/interval.c
+++ b/src/interfaces/ecpg/pgtypeslib/interval.c
@@ -60,7 +60,7 @@ ParseISO8601Number(const char *str, char **endptr, int *ipart, double *fpart)
 	if (!(isdigit((unsigned char) *str) || *str == '-' || *str == '.'))
 		return DTERR_BAD_FORMAT;
 	errno = 0;
-	val = strtod(str, endptr);
+	val = strtod_l(str, endptr, PG_C_LOCALE);
 	/* did we not see anything that looks like a double? */
 	if (*endptr == str || errno != 0)
 		return DTERR_BAD_FORMAT;
@@ -455,7 +455,7 @@ DecodeInterval(char **field, int *ftype, int nf,	/* int range, */
 				else if (*cp == '.')
 				{
 					errno = 0;
-					fval = strtod(cp, &cp);
+					fval = strtod_l(cp, &cp, PG_C_LOCALE);
 					if (*cp != '\0' || errno != 0)
 						return DTERR_BAD_FORMAT;
 
diff --git a/src/interfaces/ecpg/pgtypeslib/numeric.c b/src/interfaces/ecpg/pgtypeslib/numeric.c
index 35e7b92da40..49938543d03 100644
--- a/src/interfaces/ecpg/pgtypeslib/numeric.c
+++ b/src/interfaces/ecpg/pgtypeslib/numeric.c
@@ -1455,7 +1455,7 @@ numericvar_to_double(numeric *var, double *dp)
 	 * strtod does not reset errno to 0 in case of success.
 	 */
 	errno = 0;
-	val = strtod(tmp, &endptr);
+	val = strtod_l(tmp, &endptr, PG_C_LOCALE);
 	if (errno == ERANGE)
 	{
 		free(tmp);
diff --git a/src/port/Makefile b/src/port/Makefile
index db7c02117b0..03b55bd26b0 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	bsearch_arg.o \
 	chklocale.o \
 	inet_net_ntop.o \
+	locale.o \
 	noblock.o \
 	path.o \
 	pg_bitutils.o \
diff --git a/src/port/locale.c b/src/port/locale.c
new file mode 100644
index 00000000000..a5b0648bb8b
--- /dev/null
+++ b/src/port/locale.c
@@ -0,0 +1,78 @@
+/*-------------------------------------------------------------------------
+ *
+ * locale.c
+ *		Helper routines for thread-safe system locale usage.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/port/locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "c.h"
+
+#include <locale.h>
+#ifdef HAVE_XLOCALE_H
+#include <xlocale.h>
+#endif
+
+#ifndef WIN32
+#include <pthread.h>
+#else
+#include <synchapi.h>
+#endif
+
+static locale_t c_locale;
+
+#ifndef WIN32
+static void
+init_c_locale_once(void)
+{
+	c_locale = newlocale(LC_ALL, "C", NULL);
+}
+#else
+static BOOL
+init_c_locale_once(PINIT_ONCE once, PVOID parameter, PVOID *context)
+{
+	c_locale = _create_locale(LC_ALL, "C");
+	return true;
+}
+#endif
+
+locale_t
+pg_get_c_locale(void)
+{
+	/*
+	 * Fast path if already initialized.  This assumes that we can read a
+	 * locale_t (in practice, a pointer) without tearing in a multi-threaded
+	 * program.
+	 */
+	if (c_locale != (locale_t) 0)
+		return c_locale;
+
+	/* Make a locale_t.  It will live until process exit. */
+	{
+#ifndef WIN32
+		static pthread_once_t once = PTHREAD_ONCE_INIT;
+
+		pthread_once(&once, init_c_locale_once);
+#else
+		static INIT_ONCE once;
+		InitOnceExecuteOnce(&once, init_c_locale_once, NULL, NULL);
+#endif
+	}
+
+	/*
+	 * It is remotely possible that the locale couldn't be allocated, due to
+	 * lack of memory.  In that case (locale_t) 0 will be returned.  A caller
+	 * can test for that condition with pg_ensure_c_locale() at a convenient
+	 * time for error reporting.
+	 */
+
+	return c_locale;
+}
diff --git a/src/port/meson.build b/src/port/meson.build
index ff54b7b53e9..e21961fd739 100644
--- a/src/port/meson.build
+++ b/src/port/meson.build
@@ -5,6 +5,7 @@ pgport_sources = [
   'chklocale.c',
   'inet_net_ntop.c',
   'noblock.c',
+  'locale.c',
   'path.c',
   'pg_bitutils.c',
   'pg_strong_random.c',
diff --git a/src/port/snprintf.c b/src/port/snprintf.c
index 884f0262dd1..983d39a7729 100644
--- a/src/port/snprintf.c
+++ b/src/port/snprintf.c
@@ -109,6 +109,34 @@
 #undef	vprintf
 #undef	printf
 
+#if defined(FRONTEND)
+/*
+ * Frontend code might be multi-threaded.  When calling the system snprintf()
+ * for floats, we have to use either the snprintf_l() variant, or
+ * save-and-restore the calling thread's locale using uselocale().
+ */
+#if defined(HAVE_SNPRINTF_L)
+/* At least macOS and the BSDs have the snprintf_l() extension. */
+#define USE_SNPRINTF_L
+#elif defined(WIN32)
+/* Windows has a version with a different name and argument order. */
+#define snprintf_l(str, size, loc, format, ...) _snprintf_l(str, size, format, loc, __VA_ARGS__)
+#define USE_SNPRINTF_L
+#else
+/* We have to do a thread-safe save-and-restore around snprintf(). */
+#define NEED_USE_LOCALE
+#endif
+#else
+/*
+ * Backend code doesn't need to worry about the locales, because LC_NUMERIC is
+ * set to "C" in main() and doesn't change.  Plain old snprintf() is always OK.
+ *
+ * XXX If the backend were multithreaded, it would have to be 100% certain that
+ * no one is calling setlocale() concurrently, even transiently, to be able to
+ * keep this small optimization.
+ */
+#endif
+
 /*
  * Info about where the formatted output is going.
  *
@@ -1184,6 +1212,9 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 		 * according to == but not according to memcmp.
 		 */
 		static const double dzero = 0.0;
+#ifdef NEED_USE_LOCALE
+		locale_t	save_locale = uselocale(PG_C_LOCALE);
+#endif
 
 		if (adjust_sign((value < 0.0 ||
 						 (value == 0.0 &&
@@ -1205,7 +1236,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '*';
 			fmt[3] = type;
 			fmt[4] = '\0';
+#ifdef USE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), PG_C_LOCALE, fmt, prec, value);
+#else
 			vallen = snprintf(convert, sizeof(convert), fmt, prec, value);
+#endif
 		}
 		else
 		{
@@ -1214,6 +1249,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '\0';
 			vallen = snprintf(convert, sizeof(convert), fmt, value);
 		}
+
+#ifdef NEED_USE_LOCALE
+		uselocale(save_locale);
+#endif
+
 		if (vallen < 0)
 			goto fail;
 
@@ -1336,12 +1376,25 @@ pg_strfromd(char *str, size_t count, int precision, double value)
 		}
 		else
 		{
+#ifdef NEED_USE_LOCALE
+			locale_t	save_locale = uselocale(PG_C_LOCALE);
+#endif
+
 			fmt[0] = '%';
 			fmt[1] = '.';
 			fmt[2] = '*';
 			fmt[3] = 'g';
 			fmt[4] = '\0';
+#ifdef USE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), PG_C_LOCALE, fmt, precision, value);
+#else
 			vallen = snprintf(convert, sizeof(convert), fmt, precision, value);
+#endif
+
+#ifdef NEED_USE_LOCALE
+			uselocale(save_locale);
+#endif
+
 			if (vallen < 0)
 			{
 				target.failed = true;
-- 
2.46.0

Reply via email to