On Monday 17 November 2025 15:49:56 Martin Storsjö wrote:
> On Sat, 25 Oct 2025, Pali Rohár wrote:
>
> > Function _localtime64 is available since msvcr70.dll. For older msvcrt
> > versions provide mingw-w64 emulation via _gmtime64() function and
> > adjustment of _timezone variable (filled by _tzset() call). To check
> > whether daylight bias adjustment is required (via _dstbias variable), use
> > the _localtime32() function for the same day and month as passed timestamp
> > but with changed year to fit into the signed 32-bit timestamp. This expects
> > that the DST start and end days of the last addressable year matches also
> > for all other future years.
> > ---
> > mingw-w64-crt/Makefile.am | 2 +
> > mingw-w64-crt/lib-common/msvcrt.def.in | 2 +-
> > mingw-w64-crt/misc/_localtime64.c | 87 ++++++++++++++++++++++++++
> > 3 files changed, 90 insertions(+), 1 deletion(-)
> > create mode 100644 mingw-w64-crt/misc/_localtime64.c
> >
> > diff --git a/mingw-w64-crt/Makefile.am b/mingw-w64-crt/Makefile.am
> > index 99cccab61770..92da88f12f74 100644
> > --- a/mingw-w64-crt/Makefile.am
> > +++ b/mingw-w64-crt/Makefile.am
> > @@ -581,6 +581,7 @@ src_msvcrt32=\
> > misc/_get_fmode.c \
> > misc/_gmtime64.c \
> > misc/_initterm_e.c \
> > + misc/_localtime64.c \
> > misc/_mkgmtime32.c \
> > misc/_mkgmtime64.c \
> > misc/_set_doserrno.c \
> > @@ -892,6 +893,7 @@ src_pre_msvcr70=\
> > misc/_aligned_realloc.c \
> > misc/_ftime64.c \
> > misc/_gmtime64.c \
> > + misc/_localtime64.c \
> > misc/_time64.c \
> > misc/strtoimax.c \
> > misc/strtoumax.c \
> > diff --git a/mingw-w64-crt/lib-common/msvcrt.def.in
> > b/mingw-w64-crt/lib-common/msvcrt.def.in
> > index a1adeee91ac0..d4f50f316b3b 100644
> > --- a/mingw-w64-crt/lib-common/msvcrt.def.in
> > +++ b/mingw-w64-crt/lib-common/msvcrt.def.in
> > @@ -1157,7 +1157,7 @@ F_NON_I386(_fstat64) ; i386 _fstat64 replaced by emu
> > F_NON_I386(_ftime64) ; i386 _ftime64 replaced by emu
> > _futime64
> > F_NON_I386(_gmtime64) ; i386 _gmtime64 replaced by emu
> > -_localtime64
> > +F_NON_I386(_localtime64) ; i386 _localtime64 replaced by emu
> > _mktime64
> > F_X86_ANY(_osplatform DATA)
> > F_NON_I386(_stat64) ; i386 _stat64 replaced by emu
> > diff --git a/mingw-w64-crt/misc/_localtime64.c
> > b/mingw-w64-crt/misc/_localtime64.c
> > new file mode 100644
> > index 000000000000..85792a7be958
> > --- /dev/null
> > +++ b/mingw-w64-crt/misc/_localtime64.c
> > @@ -0,0 +1,87 @@
> > +/**
> > + * This file has no copyright assigned and is placed in the Public Domain.
> > + * This file is part of the mingw-w64 runtime package.
> > + * No warranty is given; refer to the file DISCLAIMER.PD within this
> > package.
> > + */
> > +
> > +#include <windows.h>
> > +#include <time.h>
> > +
> > +static struct tm *__cdecl emu__localtime64(const __time64_t *timeptr)
> > +{
> > + struct tm *tmptr;
> > + struct tm tm32;
> > + int local_daylight;
> > + long local_dstbias;
> > + long local_timezone;
> > + __time64_t t64;
> > + __time32_t t32;
> > +
> > + /* _tzset() initialize _daylight, _dstbias and _timezone variables,
> > + * so it needs to be called before accessing those variables.
> > + * If those variables are already initialized then _tzset() does
> > + * not need to be called again. As _tzset() is an expensive call,
> > + * guard repeated calls by static variable. As the _tzset() is a
> > + * thread-safe call, the race condition is not a problem.
> > + */
> > + {
> > + static volatile long tzset_called = 0;
> > + if (!tzset_called) {
> > + _tzset();
> > + (void)InterlockedExchange(&tzset_called, 1);
> > + }
> > + }
> > + local_daylight = *__daylight();
> > + local_dstbias = *__dstbias();
> > + local_timezone = *__timezone();
> > +
> > + /* __localtime64() for the case when the timezone does not use DST */
> > + t64 = *timeptr - local_timezone;
> > + tmptr = _gmtime64(&t64);
> > + if (!tmptr)
> > + return NULL;
> > +
> > + /* If the timezone use DST then it is needed to check if the DST is
> > active
> > + * for passed timestamp. To do that use the existing _localtime32()
> > function
> > + * and its tm_isdst member of return value. As the _localtime32()
> > function
> > + * works only for time structure which can be represented by signed
> > 32-bit
> > + * time_t type, change year of the passed timestamp, so the timestamp
> > can
> > + * be represented by 32-bit type. This expects that the DST start and
> > end
> > + * days of the last addressable year matches also for all other future
> > years.
> > + */
> > + if (local_daylight) {
> > + /* Prepare struct tm to be representable by 32-bit time_t value,
> > just by changing year */
> > + tm32 = *tmptr;
> > + if (tm32.tm_year > 2037-1900)
> > + tm32.tm_year = 2037-1900;
> > + else if (tm32.tm_year < 1971-1900)
> > + tm32.tm_year = 1971-1900;
> > +
> > + /* Use _localtime32()'s tm_isdst to determinate if the DST is
> > active for passed timestamp */
> > + t32 = _mkgmtime32(&tm32);
> > + if (t32 == -1)
> > + return NULL;
> > + tmptr = _localtime32(&t32);
> > + if (!tmptr)
> > + return NULL;
> > +
> > + /* If the DST is active for passed timestamp then recalculate the
> > struct tm according to DST bias */
> > + if (tmptr->tm_isdst) {
> > + t64 -= local_dstbias;
> > + tmptr = _gmtime64(&t64);
> > + if (!tmptr)
> > + return NULL;
> > + tmptr->tm_isdst = 1;
> > + } else {
> > + tmptr = _gmtime64(&t64);
> > + }
> > + }
>
> First off, I had my misgivings about using gmtime() for this - if we'd be on
> a CRT with more fields in "struct tm", then we would need to fill in the
> field tm_gmtoff from localtime() as well. But as I see that the MS CRTs
> don't include such fields, this is probably mostly fine.
>
> But there is one issue in the calculation of tm_isdst here.
>
> You're doing "t64 = *timeptr - local_timezone" to fake the difference
> between localtime and gmtime - while this makes the functions operate on a
> different time_t value, offset by a few hours. If operating on a time close
> to the DST switch time, then this can give the wrong results.
>
> So to compensate for that, I think something like this is needed:
>
> t32 = _mkgmtime32(&tm32);
> if (t32 == -1)
> return NULL;
> + t32 += local_timezone; /* Remove the fake timezone offset */
> tmptr = _localtime32(&t32);
> if (!tmptr)
> return NULL;
>
> Essentially, the _localtime32 call we use for determining whether we are in
> DST or not, would need to operate on the original, non-offset time_t value.
> (And for out of range values, the logic above for switching to a different
> year, but for the same date/time.)
>
> // Martin
Thank you for doing this detailed code review. This time logic is not
easy and that is why I added comments to explain this code a bit more.
About fake timezone offset, you are right there is missing that
"t32 += local_timezone;" line.
Another point of view how to observe that missing line: time_t argument
or return value for localtime() and mktime() is always in UTC. So
tracing the code can show that "t32" before the _localtime32() contains
timestamp in local timezone, not in UTC. So conversion from local time
to UTC is needed and that missing line is doing it.
_______________________________________________
Mingw-w64-public mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/mingw-w64-public