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