Skip to content

Commit fcbfaae

Browse files
committed
Implement TimeZoneIf with Windows Registry
This commit introduces a new implementation of the TimeZoneIf interface that is compatible with Windows Time APIs. Background: Here's a quick comparison of how time zone libraries are implemented on modern Windows: * <chrono> in Microsoft STL -> Uses system "icu.dll" [1] if available [2]. * System.TimeZoneInfo in .NET -> Uses Windows Registry as the source of time zone information. * CCTZ -> Uses zoneinfo if found in TZDIR or ZoneInfoSourceFactory. This commit brings what .NET does to CCTZ. The major advantages of using Windows Registry instead of "icu.dll" are: * It gives the full consistency with the Windows Time APIs. * The data has proven to be updated via Windows Update in a good cadence [3]. For instance, tzdb 2022b is still used in "icu.dll" shipped with Windows 11 24H2. Implementation Note: Windows uses per-year time zone information as follows [4]: struct REG_TZI_FORMAT { // Base offset in minutes, where UTC == local time + Bias. LONG Bias; // Additional offset in minutes applied to standard time. LONG StandardBias; // Additional offset in minutes applied to DST. LONG DaylightBias; // Localtime (in the previous offset) when the standard time begins. SYSTEMTIME StandardDate; // Localtime (in the previous offset) when the DST begins. SYSTEMTIME DaylightDate; }; For each time zone ID, one default REG_TZI_FORMAT entry and optional year-keyed REG_TZI_FORMAT entries are stored under the following registry keys: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\<zone_id> Years that are not explicitly covered by a year-keyed entry are governed by the default entry or the earliest year-keyed entry, respectively. SYSTEMTIME is defined as follows: struct SYSTEMTIME { WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; }; There are special rules of SYSTEMTIME used in REG_TZI_FORMAT [4]: * If wMonth is zero, the transition does not occur. * If wYear is not zero, the transition date is absolute. * If wYear is zero and wMonth is not zero, the transition rule is recurring every year based on wDayOfWeek. wDay indicates the occurrence of the day of the week within the month. wDay == 5 means the final occurrence during the month. * All fields are in the local time in the previous offset rather than the new offset. * (wHour, wMinute, wSecond, wMilliseconds) == (23, 59, 59, 999) is a special case that indicates the transition occurs at the end of the day (i.e., 00:00:00.000 of the next day) [5]. With above, we should be able to implement the TimeZoneIf interface. Note that only the data loading steps from the Windows Registry need to be Windows-specific. The rest of the implementation is designed to be platform-independent in case we want to perform further testing and code analysis as needed. To run the basic tests, run the following command. bazelisk test //:time_zone_win_test How to build: To actually use it as the fallback mechanism, CCTZ_USE_WIN_REGISTRY_FALLBACK macro needs to be defined when building cctz on Windows. bazelisk run :time_tool \ --cxxopt=/DCCTZ_USE_WIN_REGISTRY_FALLBACK bazelisk test //:time_zone_lookup_test \ --cxxopt=/DCCTZ_USE_WIN_REGISTRY_FALLBACK [1]: https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu- [2]: Put PR link to MS STL 1789 here. [3]: https://techcommunity.microsoft.com/category/windows/blog/dstblog [4]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information [5]: https://stackoverflow.com/a/47106207
1 parent 46d62c0 commit fcbfaae

12 files changed

+1502
-26
lines changed

BUILD

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,16 @@ cc_library(
5656
"src/time_zone_lookup.cc",
5757
"src/time_zone_posix.cc",
5858
"src/time_zone_posix.h",
59+
"src/time_zone_win.cc",
60+
"src/time_zone_win.h",
5961
"src/tzfile.h",
6062
"src/zone_info_source.cc",
6163
] + select({
6264
"@platforms//os:windows": [
6365
"src/time_zone_name_win.cc",
6466
"src/time_zone_name_win.h",
67+
"src/time_zone_win_loader.cc",
68+
"src/time_zone_win_loader.h",
6569
],
6670
"//conditions:default": [],
6771
}),
@@ -73,6 +77,7 @@ cc_library(
7377
linkopts = select({
7478
"@platforms//os:osx": ["-Wl,-framework,CoreFoundation"],
7579
"@platforms//os:ios": ["-Wl,-framework,CoreFoundation"],
80+
"@platforms//os:windows": ["advapi32.lib"],
7681
"//conditions:default": [],
7782
}),
7883
visibility = ["//visibility:public"],
@@ -147,6 +152,18 @@ cc_test(
147152
],
148153
)
149154

155+
cc_test(
156+
name = "time_zone_win_test",
157+
size = "small",
158+
srcs = ["src/time_zone_win_test.cc"],
159+
deps = [
160+
":civil_time",
161+
":time_zone",
162+
"@googletest//:gtest",
163+
"@googletest//:gtest_main",
164+
],
165+
)
166+
150167
### benchmarks
151168

152169
cc_test(

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,14 @@ add_library(cctz
8383
src/time_zone_lookup.cc
8484
src/time_zone_posix.cc
8585
src/time_zone_posix.h
86+
src/time_zone_win.cc
87+
src/time_zone_win.h
8688
src/tzfile.h
8789
src/zone_info_source.cc
8890
$<$<PLATFORM_ID:Windows>:src/time_zone_name_win.cc>
8991
$<$<PLATFORM_ID:Windows>:src/time_zone_name_win.h>
92+
$<$<PLATFORM_ID:Windows>:src/time_zone_win_loader.cc>
93+
$<$<PLATFORM_ID:Windows>:src/time_zone_win_loader.h>
9094
${CCTZ_HDRS}
9195
)
9296
cctz_target_set_cxx_standard(cctz)
@@ -100,6 +104,9 @@ set_target_properties(cctz PROPERTIES
100104
if(APPLE)
101105
target_link_libraries(cctz PUBLIC ${CoreFoundation})
102106
endif()
107+
if(WIN32)
108+
target_link_libraries(cctz PUBLIC advapi32.lib)
109+
endif()
103110
add_library(cctz::cctz ALIAS cctz)
104111

105112
if (BUILD_TOOLS)
@@ -148,6 +155,15 @@ if (BUILD_TESTING)
148155
)
149156
add_test(time_zone_format_test time_zone_format_test)
150157

158+
add_executable(time_zone_win_test src/time_zone_win_test.cc)
159+
cctz_target_set_cxx_standard(time_zone_win_test)
160+
target_link_libraries(time_zone_win_test
161+
cctz::cctz
162+
${CMAKE_THREAD_LIBS_INIT}
163+
GMock::Main
164+
)
165+
add_test(time_zone_win_test time_zone_win_test)
166+
151167
# tests runs on testdata
152168
set_property(
153169
TEST

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ zones in a simple and correct manner. The libraries in CCTZ are:
1818
These libraries are currently known to work on **Linux**, **Mac OS X**, and
1919
**Android**.
2020

21-
They will also work on **Windows** if you install the zoneinfo files. We are
22-
interested, though, in an implementation of the cctz::TimeZoneIf interface that
23-
calls the Windows time APIs instead. Please contact us if you're interested in
24-
contributing.
21+
They will also work on **Windows** if you install the zoneinfo files. You can
22+
also specify a built-time macro `CCTZ_USE_WIN_REGISTRY_FALLBACK` to let CCTZ
23+
fall back to time zone information stored in the Windows
24+
[registry](https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information#remarks)
25+
when the zoneinfo files are not available.
2526

2627
# Getting Started
2728

src/time_zone_if.cc

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
#include "time_zone_info.h"
1717
#include "time_zone_libc.h"
1818

19+
#if defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK)
20+
#include "time_zone_win.h"
21+
#include "time_zone_win_loader.h"
22+
#endif // defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK)
23+
1924
namespace cctz {
2025

2126
std::unique_ptr<TimeZoneIf> TimeZoneIf::UTC() {
@@ -31,8 +36,17 @@ std::unique_ptr<TimeZoneIf> TimeZoneIf::Make(const std::string& name) {
3136
return TimeZoneLibC::Make(name.substr(5));
3237
}
3338

34-
// Otherwise use the "zoneinfo" implementation.
35-
return TimeZoneInfo::Make(name);
39+
// Attempt to use the "zoneinfo" implementation.
40+
std::unique_ptr<TimeZoneIf> zone_info = TimeZoneInfo::Make(name);
41+
42+
#if defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK)
43+
if (!zone_info) {
44+
// Attempt to fall back to Win32 Registry Implementation.
45+
zone_info = MakeTimeZoneFromWinRegistry(LoadWinTimeZoneRegistry(name));
46+
}
47+
#endif // defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK)
48+
49+
return zone_info;
3650
}
3751

3852
// Defined out-of-line to avoid emitting a weak vtable in all TUs.

src/time_zone_lookup_test.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,32 @@ TEST(MakeTime, SysSecondsLimits) {
451451
}
452452
}
453453

454+
TEST(MakeTime, LookupKind) {
455+
const time_zone tz = LoadZone("America/Los_Angeles");
456+
457+
// Spring 1:59:59 -> 3:00:00
458+
auto lookup = tz.lookup(civil_second(2013, 3, 10, 1, 59, 59));
459+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE);
460+
lookup = tz.lookup(civil_second(2013, 3, 10, 2, 0, 0));
461+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::SKIPPED);
462+
lookup = tz.lookup(civil_second(2013, 3, 10, 2, 15, 0));
463+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::SKIPPED);
464+
lookup = tz.lookup(cctz::civil_second(2013, 6, 1, 3, 0, 0));
465+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE);
466+
467+
// Fall 1:59:59 -> 1:00:00
468+
lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 0, 59, 59));
469+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE);
470+
lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 1, 0, 0));
471+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::REPEATED);
472+
lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 1, 30, 0));
473+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::REPEATED);
474+
lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 1, 59, 59));
475+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::REPEATED);
476+
lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 2, 0, 0));
477+
EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE);
478+
}
479+
454480
TEST(MakeTime, LocalTimeLibC) {
455481
// Checks that cctz and libc agree on transition points in [1970:2037].
456482
//

src/time_zone_name_win.cc

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,14 @@ bool U_SUCCESS(UErrorCode error) { return error <= U_ZERO_ERROR; }
4545
using ucal_getTimeZoneIDForWindowsID_func = std::int32_t(__cdecl*)(
4646
const UChar* winid, std::int32_t len, const char* region, UChar* id,
4747
std::int32_t id_capacity, UErrorCode* status);
48+
using ucal_getWindowsTimeZoneID_func =
49+
std::int32_t(__cdecl*)(const UChar* id, std::int32_t len, UChar* winid,
50+
std::int32_t winid_capacity, UErrorCode* status);
4851

4952
std::atomic<bool> g_unavailable;
5053
std::atomic<ucal_getTimeZoneIDForWindowsID_func>
5154
g_ucal_getTimeZoneIDForWindowsID;
55+
std::atomic<ucal_getWindowsTimeZoneID_func> g_ucal_getWindowsTimeZoneID;
5256

5357
template <typename T> static T AsProcAddress(HMODULE module, const char* name) {
5458
static_assert(
@@ -73,6 +77,49 @@ std::wstring GetSystem32Dir() {
7377
return result;
7478
}
7579

80+
bool LoadIcuFunctionsInternal() {
81+
const std::wstring system32_dir = GetSystem32Dir();
82+
if (system32_dir.empty()) {
83+
g_unavailable.store(true, std::memory_order_relaxed);
84+
return false;
85+
}
86+
87+
// Here LoadLibraryExW(L"icu.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) does
88+
// not work if "icu.dll" is already loaded from somewhere other than the
89+
// system32 directory. Specifying the full path with LoadLibraryW is more
90+
// reliable.
91+
const std::wstring icu_dll_path = system32_dir + L"\\icu.dll";
92+
const HMODULE icu_dll = ::LoadLibraryW(icu_dll_path.c_str());
93+
if (icu_dll == nullptr) {
94+
g_unavailable.store(true, std::memory_order_relaxed);
95+
return false;
96+
}
97+
98+
const auto ucal_getTimeZoneIDForWindowsIDRef =
99+
AsProcAddress<ucal_getTimeZoneIDForWindowsID_func>(
100+
icu_dll, "ucal_getTimeZoneIDForWindowsID");
101+
if (ucal_getTimeZoneIDForWindowsIDRef != nullptr) {
102+
g_unavailable.store(true, std::memory_order_relaxed);
103+
return false;
104+
}
105+
106+
g_ucal_getTimeZoneIDForWindowsID.store(ucal_getTimeZoneIDForWindowsIDRef,
107+
std::memory_order_relaxed);
108+
109+
const auto ucal_getWindowsTimeZoneIDRef =
110+
AsProcAddress<ucal_getWindowsTimeZoneID_func>(
111+
icu_dll, "ucal_getWindowsTimeZoneID");
112+
if (ucal_getWindowsTimeZoneIDRef != nullptr) {
113+
g_unavailable.store(true, std::memory_order_relaxed);
114+
return false;
115+
}
116+
117+
g_ucal_getWindowsTimeZoneID.store(ucal_getWindowsTimeZoneIDRef,
118+
std::memory_order_relaxed);
119+
120+
return true;
121+
}
122+
76123
ucal_getTimeZoneIDForWindowsID_func LoadIcuGetTimeZoneIDForWindowsID() {
77124
// This function is intended to be lock free to avoid potential deadlocks
78125
// with loader-lock taken inside LoadLibraryW. As LoadLibraryW and
@@ -92,35 +139,34 @@ ucal_getTimeZoneIDForWindowsID_func LoadIcuGetTimeZoneIDForWindowsID() {
92139
}
93140
}
94141

95-
const std::wstring system32_dir = GetSystem32Dir();
96-
if (system32_dir.empty()) {
97-
g_unavailable.store(true, std::memory_order_relaxed);
142+
if (!LoadIcuFunctionsInternal()) {
98143
return nullptr;
99144
}
100145

101-
// Here LoadLibraryExW(L"icu.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) does
102-
// not work if "icu.dll" is already loaded from somewhere other than the
103-
// system32 directory. Specifying the full path with LoadLibraryW is more
104-
// reliable.
105-
const std::wstring icu_dll_path = system32_dir + L"\\icu.dll";
106-
const HMODULE icu_dll = ::LoadLibraryW(icu_dll_path.c_str());
107-
if (icu_dll == nullptr) {
108-
g_unavailable.store(true, std::memory_order_relaxed);
146+
return g_ucal_getTimeZoneIDForWindowsID.load(std::memory_order_relaxed);
147+
}
148+
149+
ucal_getWindowsTimeZoneID_func LoadIcuGetWindowsTimeZoneID() {
150+
// This function is intended to be lock. See the comment in
151+
// LoadIcuGetTimeZoneIDForWindowsID() for details.
152+
153+
if (g_unavailable.load(std::memory_order_relaxed)) {
109154
return nullptr;
110155
}
111156

112-
const auto ucal_getTimeZoneIDForWindowsIDRef =
113-
AsProcAddress<ucal_getTimeZoneIDForWindowsID_func>(
114-
icu_dll, "ucal_getTimeZoneIDForWindowsID");
115-
if (ucal_getTimeZoneIDForWindowsIDRef != nullptr) {
116-
g_unavailable.store(true, std::memory_order_relaxed);
117-
return nullptr;
157+
{
158+
const auto ucal_getWindowsTimeZoneID =
159+
g_ucal_getWindowsTimeZoneID.load(std::memory_order_relaxed);
160+
if (ucal_getWindowsTimeZoneID != nullptr) {
161+
return ucal_getWindowsTimeZoneID;
162+
}
118163
}
119164

120-
g_ucal_getTimeZoneIDForWindowsID.store(ucal_getTimeZoneIDForWindowsIDRef,
121-
std::memory_order_relaxed);
165+
if (!LoadIcuFunctionsInternal()) {
166+
return nullptr;
167+
}
122168

123-
return ucal_getTimeZoneIDForWindowsIDRef;
169+
return g_ucal_getWindowsTimeZoneID.load(std::memory_order_relaxed);
124170
}
125171

126172
// Convert wchar_t array (UTF-16) to UTF-8 string
@@ -174,4 +220,33 @@ std::string GetWindowsLocalTimeZone() {
174220
}
175221
}
176222

223+
std::wstring ConvertToWindowsTimeZoneId(const std::wstring& iana_name) {
224+
const auto getWindowsTimeZoneID = LoadIcuGetWindowsTimeZoneID();
225+
if (getWindowsTimeZoneID == nullptr) {
226+
return std::wstring();
227+
}
228+
if (iana_name.size() > std::numeric_limits<std::int32_t>::max()) {
229+
return std::wstring();
230+
}
231+
const std::int32_t iana_name_length =
232+
static_cast<std::int32_t>(iana_name.size());
233+
234+
std::wstring result;
235+
std::size_t len = std::max<std::size_t>(
236+
std::min<size_t>(result.capacity(), std::numeric_limits<int>::max()), 1);
237+
for (;;) {
238+
UErrorCode status = U_ZERO_ERROR;
239+
result.resize(len);
240+
len = static_cast<std::size_t>(getWindowsTimeZoneID(
241+
iana_name.c_str(), iana_name_length, &result[0], static_cast<int>(len),
242+
&status));
243+
if (U_SUCCESS(status)) {
244+
return result;
245+
}
246+
if (status != U_BUFFER_OVERFLOW_ERROR) {
247+
return std::wstring();
248+
}
249+
}
250+
}
251+
177252
} // namespace cctz

src/time_zone_name_win.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ namespace cctz {
2424
// where "icu.dll" is not available in the System32 directory.
2525
std::string GetWindowsLocalTimeZone();
2626

27+
// Converts IANA time zone name to Windows time zone ID, or the empty string on
28+
// failure. Not supported on Windows 10 1809 and earlier, where "icu.dll" is not
29+
// available in the System32 directory.
30+
std::wstring ConvertToWindowsTimeZoneId(const std::wstring& iana_name);
31+
2732
} // namespace cctz
2833

2934
#endif // CCTZ_TIME_ZONE_NAME_WIN_H_

0 commit comments

Comments
 (0)