Skip to content

Commit eae963d

Browse files
JanWielemakerclaude
andcommitted
FIXED: #1093 PL_initialise() no longer mutates the process LC_NUMERIC locale
initLocale() used to call setlocale(LC_NUMERIC, "") to bootstrap the default PL_locale from localeconv(). As a side effect it changed the embedder's process-wide decimal separator, breaking atof() and printf("%f") in host code that expects the C locale. Read the environment's numeric conventions via newlocale()/uselocale() (POSIX 2008) instead, with a save/setlocale/restore fallback under L_LOCALE for platforms without them. locale_create/3 uses the same helper. Also fix a latent bug in update_locale() that only surfaced with the above change: Prolog setlocale/3 for unrelated categories (notably LC_MESSAGES from os_user_lang/1 during boot) was unconditionally re-reading localeconv() and clobbering the decimal_point picked up at init time. Gate the refresh on LC_NUMERIC / LC_ALL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b9a860c commit eae963d

5 files changed

Lines changed: 101 additions & 21 deletions

File tree

cmake/Config.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ check_function_exists(setlocale HAVE_SETLOCALE)
193193
check_function_exists(mbsnrtowcs HAVE_MBSNRTOWCS)
194194
check_function_exists(mbcasescoll HAVE_MBCASESCOLL)
195195
check_function_exists(localeconv HAVE_LOCALECONV)
196+
check_function_exists(newlocale HAVE_NEWLOCALE)
197+
check_function_exists(uselocale HAVE_USELOCALE)
196198
check_function_exists(wcsdup HAVE_WCSDUP)
197199
check_function_exists(wcsxfrm HAVE_WCSXFRM)
198200
check_function_exists(wcwidth HAVE_WCWIDTH)

man/builtin.plx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7952,7 +7952,12 @@ integers from digit groups was discussed on the SWI-Prolog mailinglist.
79527952
Most input in this discussion is from Ulrich Neumerkel and Richard
79537953
O'Keefe. The predicates in this section were designed by Jan
79547954
Wielemaker.} The system creates a default locale object named
7955-
\const{default} from the system locale. This locale is used as the
7955+
\const{default} from the environment's \const{LC_NUMERIC} locale
7956+
without modifying the process locale. This is important for
7957+
applications that embed SWI-Prolog: PL_initialise() will not change
7958+
the process-wide decimal separator used by \cfuncref{atof}{} or
7959+
\cfuncref{printf}{} in the host application. See also
7960+
\secref{embed-locale}. The \const{default} locale is used as the
79567961
initial locale for the three standard streams as well as the
79577962
\const{main} thread. Locale sensitive output predicates such as format/3
79587963
get their locale from the stream to which they deliver their output. New

man/foreign.plx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5321,6 +5321,25 @@ signal may be changed using the \cmdlineoption{--sigalert=NUM} option or
53215321
disabled using \verb$--sigalert=0$.
53225322
53235323
5324+
\subsection{Locale and PL_initialise()} \label{sec:embed-locale}
5325+
5326+
PL_initialise() reads the numeric conventions (decimal point, thousands
5327+
separator, digit grouping) of the environment's \const{LC_NUMERIC}
5328+
locale for use by locale-sensitive predicates such as format/3 with
5329+
\const{~:d} and \const{~:f} directives. See \secref{locale}. It does
5330+
so without calling \cfuncref{setlocale}{}, so the embedder's process
5331+
locale is left untouched and functions such as \cfuncref{atof}{} and
5332+
\cfuncref{printf}{}(\const{"\%f"}) continue to use the decimal point
5333+
that was in effect before PL_initialise() was called.
5334+
5335+
PL_initialise() does call \cfuncref{setlocale}{} for \const{LC_CTYPE}
5336+
(to pick up the multibyte encoding used for atoms and streams),
5337+
\const{LC_TIME} and \const{LC_COLLATE}. Embedders that require a
5338+
strict \const{"C"} locale for these categories should either arrange
5339+
the environment before starting the process or reset the desired
5340+
categories after PL_initialise() returns.
5341+
5342+
53245343
53255344
\section{Linking embedded applications using swipl-ld} \label{sec:plld}
53265345

src/config.h.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
#cmakedefine HAVE_MP_BITCNT_T @HAVE_MP_BITCNT_T@
9494
#cmakedefine HAVE_MTRACE @HAVE_MTRACE@
9595
#cmakedefine HAVE_NANOSLEEP @HAVE_NANOSLEEP@
96+
#cmakedefine HAVE_NEWLOCALE @HAVE_NEWLOCALE@
97+
#cmakedefine HAVE_USELOCALE @HAVE_USELOCALE@
9698
#cmakedefine HAVE_NCURSES_CURSES_H @HAVE_NCURSES_CURSES_H@
9799
#cmakedefine HAVE_NCURSES_TERM_H @HAVE_NCURSES_TERM_H@
98100
#cmakedefine HAVE_NDIR_H @HAVE_NDIR_H@

src/os/pl-locale.c

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,53 @@ init_locale_strings(PL_locale *l, struct lconv *conv)
119119
}
120120

121121

122+
/* Fill L with the LC_NUMERIC conventions for LNAME ("" == environment)
123+
* without mutating the process-wide locale. Returns true on success.
124+
*
125+
* PL_initialise() historically called setlocale(LC_NUMERIC, "") to
126+
* bootstrap the default PL_locale from localeconv(). That silently
127+
* changed the embedder's process locale, breaking atof() and printf
128+
* "%f" in locales that use "," as the decimal separator (issue #1093).
129+
* The POSIX-2008 newlocale()/uselocale() pair changes only the
130+
* calling thread's locale, so localeconv() reads the requested
131+
* conventions without touching the process locale. Where those are
132+
* unavailable we fall back to a save/setlocale/restore dance under
133+
* L_LOCALE, which is safe during initialisation (single-threaded) and
134+
* during locale_create/3 (already serialised on L_LOCALE).
135+
*/
136+
137+
static bool
138+
fill_locale_from_name(PL_locale *l, const char *lname)
139+
{ bool rc;
140+
141+
#if defined(HAVE_NEWLOCALE) && defined(HAVE_USELOCALE)
142+
locale_t loc = newlocale(LC_NUMERIC_MASK, lname, (locale_t)0);
143+
if ( !loc )
144+
return false;
145+
locale_t old = uselocale(loc);
146+
rc = init_locale_strings(l, localeconv());
147+
uselocale(old);
148+
freelocale(loc);
149+
#else
150+
PL_LOCK(L_LOCALE);
151+
const char *saved = setlocale(LC_NUMERIC, NULL);
152+
char *old = saved ? strdup(saved) : NULL;
153+
if ( !setlocale(LC_NUMERIC, lname) )
154+
{ free(old);
155+
PL_UNLOCK(L_LOCALE);
156+
return false;
157+
}
158+
rc = init_locale_strings(l, localeconv());
159+
if ( old )
160+
setlocale(LC_NUMERIC, old);
161+
free(old);
162+
PL_UNLOCK(L_LOCALE);
163+
#endif
164+
165+
return rc;
166+
}
167+
168+
122169
static PL_locale *
123170
new_locale(PL_locale *proto)
124171
{ PL_locale *new = PL_malloc(sizeof(*new));
@@ -131,8 +178,8 @@ new_locale(PL_locale *proto)
131178
{ new->decimal_point = wcsdup(proto->decimal_point);
132179
new->thousands_sep = wcsdup(proto->thousands_sep);
133180
new->grouping = strdup(proto->grouping);
134-
} else
135-
{ init_locale_strings(new, localeconv());
181+
} else if ( !fill_locale_from_name(new, "") )
182+
{ init_locale_strings(new, NULL);
136183
}
137184
}
138185

@@ -161,8 +208,14 @@ free_locale(PL_locale *l)
161208

162209
static void
163210
update_locale(PL_locale *l, int category, const char *locale)
164-
{ free_locale_strings(l);
165-
init_locale_strings(l, localeconv());
211+
{ /* Only numeric conventions live in a PL_locale. Refreshing them for
212+
* unrelated categories such as LC_MESSAGES would clobber the values
213+
* picked up at init time from the environment's LC_NUMERIC.
214+
*/
215+
if ( category == LC_NUMERIC || category == LC_ALL )
216+
{ free_locale_strings(l);
217+
init_locale_strings(l, localeconv());
218+
}
166219
}
167220

168221

@@ -718,19 +771,17 @@ PRED_IMPL("locale_create", 3, locale_create, 0)
718771
{ if ( strcmp(lname, "default") == 0 )
719772
{ new = new_locale(NULL);
720773
} else
721-
{ const char *old;
722-
723-
PL_LOCK(L_LOCALE);
724-
if ( (old=setlocale(LC_NUMERIC, lname)) )
725-
{ new = new_locale(NULL);
726-
setlocale(LC_NUMERIC, old);
727-
}
728-
PL_UNLOCK(L_LOCALE);
729-
if ( !old )
730-
{ if ( errno == ENOENT )
731-
return PL_existence_error("locale", A2);
732-
else
733-
return PL_error(NULL, 0, MSG_ERRNO, ERR_SYSCALL, "setlocale");
774+
{ if ( (new = PL_malloc(sizeof(*new))) )
775+
{ memset(new, 0, sizeof(*new));
776+
new->magic = LOCALE_MAGIC;
777+
if ( !fill_locale_from_name(new, lname) )
778+
{ PL_free(new);
779+
new = NULL;
780+
if ( errno == ENOENT )
781+
return PL_existence_error("locale", A2);
782+
else
783+
return PL_error(NULL, 0, MSG_ERRNO, ERR_SYSCALL, "setlocale");
784+
}
734785
}
735786
}
736787
} else
@@ -881,9 +932,10 @@ initLocale(void)
881932
{ GET_LD
882933
PL_locale *def;
883934

884-
if ( !setlocale(LC_NUMERIC, "") )
885-
{ DEBUG(0, Sdprintf("Failed to set LC_NUMERIC locale\n"));
886-
}
935+
/* new_locale(NULL) queries the environment's LC_NUMERIC conventions
936+
* via read_lconv() without touching the process locale, so an
937+
* embedder's atof()/printf("%f") remain deterministic (issue #1093).
938+
*/
887939

888940
if ( (def = new_locale(NULL)) )
889941
{ alias_locale(def, ATOM_default);

0 commit comments

Comments
 (0)