Skip to content

Commit 2ebbe0d

Browse files
authored
Remove WinRT deps from cctz::local_time_zone (google#316)
This reworks our previous commit [1], which aimed to address google#53 by implementing cctz::local_time_zone with WinRT API on Windows. The issue is there is no guarantee that RoInitialize can be safely called when cctz::local_time_zone gets called [2]. In theory cctz can have its own thread to safely call RoInitialize and WinRT APIs, but such an approach would make the code much more complex and fragile. This commit takes advantage of newly exposed ICU APIs that do not require COM/WinRT initialization on Windows 10 1903 and later [3]. To maintain binary compatibility with earlier versions of Windows, the DLL and function pointer are still loaded dynamically. [1]: 1bb996c [2]: google/mozc#856 [3]: https://devblogs.microsoft.com/oldnewthing/20210527-00/?p=105255
1 parent 74e787f commit 2ebbe0d

File tree

1 file changed

+81
-110
lines changed

1 file changed

+81
-110
lines changed

src/time_zone_lookup.cc

Lines changed: 81 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,23 @@
3131
#endif
3232

3333
#if defined(_WIN32)
34-
#include <sdkddkver.h>
35-
// Include only when the SDK is for Windows 10 (and later), and the binary is
36-
// targeted for Windows XP and later.
37-
// Note: The Windows SDK added windows.globalization.h file for Windows 10, but
38-
// MinGW did not add it until NTDDI_WIN10_NI (SDK version 10.0.22621.0).
39-
#if ((defined(_WIN32_WINNT_WIN10) && !defined(__MINGW32__)) || \
40-
(defined(NTDDI_WIN10_NI) && NTDDI_VERSION >= NTDDI_WIN10_NI)) && \
41-
(_WIN32_WINNT >= _WIN32_WINNT_WINXP)
34+
// Include only when <icu.h> is available.
35+
// https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu-
36+
// https://devblogs.microsoft.com/oldnewthing/20210527-00/?p=105255
37+
#if defined(__has_include)
38+
#if __has_include(<icu.h>)
4239
#define USE_WIN32_LOCAL_TIME_ZONE
43-
#include <roapi.h>
44-
#include <tchar.h>
45-
#include <wchar.h>
46-
#include <windows.globalization.h>
40+
#pragma push_macro("NTDDI_VERSION")
41+
// ucal_getTimeZoneIDForWindowsID is available on Windows 10 RS3 and later.
42+
#define NTDDI_VERSION 0x0A000004 // == NTDDI_WIN10_RS3
4743
#include <windows.h>
48-
#include <winstring.h>
49-
#endif
50-
#endif
44+
#include <icu.h>
45+
#include <timezoneapi.h>
46+
#pragma pop_macro("NTDDI_VERSION")
47+
#include <atomic>
48+
#endif // __has_include(<icu.h>)
49+
#endif // __has_include
50+
#endif // _WIN32
5151

5252
#include <cstdlib>
5353
#include <cstring>
@@ -60,80 +60,78 @@ namespace cctz {
6060

6161
namespace {
6262
#if defined(USE_WIN32_LOCAL_TIME_ZONE)
63-
// Calls the WinRT Calendar.GetTimeZone method to obtain the IANA ID of the
64-
// local time zone. Returns an empty vector in case of an error.
65-
std::string win32_local_time_zone(const HMODULE combase) {
66-
std::string result;
67-
const auto ro_activate_instance =
68-
reinterpret_cast<decltype(&RoActivateInstance)>(
69-
GetProcAddress(combase, "RoActivateInstance"));
70-
if (!ro_activate_instance) {
71-
return result;
72-
}
73-
const auto windows_create_string_reference =
74-
reinterpret_cast<decltype(&WindowsCreateStringReference)>(
75-
GetProcAddress(combase, "WindowsCreateStringReference"));
76-
if (!windows_create_string_reference) {
77-
return result;
78-
}
79-
const auto windows_delete_string =
80-
reinterpret_cast<decltype(&WindowsDeleteString)>(
81-
GetProcAddress(combase, "WindowsDeleteString"));
82-
if (!windows_delete_string) {
83-
return result;
84-
}
85-
const auto windows_get_string_raw_buffer =
86-
reinterpret_cast<decltype(&WindowsGetStringRawBuffer)>(
87-
GetProcAddress(combase, "WindowsGetStringRawBuffer"));
88-
if (!windows_get_string_raw_buffer) {
89-
return result;
63+
// True if we have already failed to load the API.
64+
static std::atomic_bool g_ucal_getTimeZoneIDForWindowsIDUnavailable;
65+
static std::atomic<decltype(ucal_getTimeZoneIDForWindowsID)*>
66+
g_ucal_getTimeZoneIDForWindowsIDRef;
67+
68+
std::string win32_local_time_zone() {
69+
// If we have already failed to load the API, then just give up.
70+
if (g_ucal_getTimeZoneIDForWindowsIDUnavailable.load()) {
71+
return "";
9072
}
9173

92-
// The string returned by WindowsCreateStringReference doesn't need to be
93-
// deleted.
94-
HSTRING calendar_class_id;
95-
HSTRING_HEADER calendar_class_id_header;
96-
HRESULT hr = windows_create_string_reference(
97-
RuntimeClass_Windows_Globalization_Calendar,
98-
sizeof(RuntimeClass_Windows_Globalization_Calendar) / sizeof(wchar_t) - 1,
99-
&calendar_class_id_header, &calendar_class_id);
100-
if (FAILED(hr)) {
101-
return result;
102-
}
74+
auto ucal_getTimeZoneIDForWindowsIDFunc =
75+
g_ucal_getTimeZoneIDForWindowsIDRef.load();
76+
if (ucal_getTimeZoneIDForWindowsIDFunc == nullptr) {
77+
// If we have already failed to load the API, then just give up.
78+
if (g_ucal_getTimeZoneIDForWindowsIDUnavailable.load()) {
79+
return "";
80+
}
10381

104-
IInspectable* calendar;
105-
hr = ro_activate_instance(calendar_class_id, &calendar);
106-
if (FAILED(hr)) {
107-
return result;
82+
const HMODULE icudll = ::LoadLibraryExW(L"icu.dll", nullptr,
83+
LOAD_LIBRARY_SEARCH_SYSTEM32);
84+
85+
if (icudll == nullptr) {
86+
g_ucal_getTimeZoneIDForWindowsIDUnavailable.store(true);
87+
return "";
88+
}
89+
90+
ucal_getTimeZoneIDForWindowsIDFunc =
91+
reinterpret_cast<decltype(ucal_getTimeZoneIDForWindowsID)*>(
92+
::GetProcAddress(icudll, "ucal_getTimeZoneIDForWindowsID"));
93+
94+
if (ucal_getTimeZoneIDForWindowsIDFunc == nullptr) {
95+
g_ucal_getTimeZoneIDForWindowsIDUnavailable.store(true);
96+
return "";
97+
}
98+
// store-race is not a problem here, because ::GetProcAddress() returns the
99+
// same address for the same function in the same DLL.
100+
g_ucal_getTimeZoneIDForWindowsIDRef.store(
101+
ucal_getTimeZoneIDForWindowsIDFunc);
102+
103+
// We intentionally do not call ::FreeLibrary() here to avoid frequent DLL
104+
// loadings and unloading. As "icu.dll" is a system library, keeping it on
105+
// memory is supposed to have no major drawback.
108106
}
109107

110-
ABI::Windows::Globalization::ITimeZoneOnCalendar* time_zone;
111-
hr = calendar->QueryInterface(IID_PPV_ARGS(&time_zone));
112-
if (FAILED(hr)) {
113-
calendar->Release();
114-
return result;
108+
DYNAMIC_TIME_ZONE_INFORMATION info = {};
109+
if (::GetDynamicTimeZoneInformation(&info) == TIME_ZONE_ID_INVALID) {
110+
return "";
115111
}
116112

117-
HSTRING tz_hstr;
118-
hr = time_zone->GetTimeZone(&tz_hstr);
119-
if (SUCCEEDED(hr)) {
120-
UINT32 wlen;
121-
const PCWSTR tz_wstr = windows_get_string_raw_buffer(tz_hstr, &wlen);
122-
if (tz_wstr) {
123-
const int size =
124-
WideCharToMultiByte(CP_UTF8, 0, tz_wstr, static_cast<int>(wlen),
125-
nullptr, 0, nullptr, nullptr);
126-
result.resize(static_cast<size_t>(size));
127-
WideCharToMultiByte(CP_UTF8, 0, tz_wstr, static_cast<int>(wlen),
128-
&result[0], size, nullptr, nullptr);
129-
}
130-
windows_delete_string(tz_hstr);
113+
UChar buffer[128];
114+
UErrorCode status = U_ZERO_ERROR;
115+
const auto num_chars_in_buffer = ucal_getTimeZoneIDForWindowsIDFunc(
116+
reinterpret_cast<const UChar*>(info.TimeZoneKeyName), -1, nullptr,
117+
buffer, ARRAYSIZE(buffer), &status);
118+
if (status != U_ZERO_ERROR || num_chars_in_buffer <= 0 ||
119+
num_chars_in_buffer > ARRAYSIZE(buffer)) {
120+
return "";
131121
}
132-
time_zone->Release();
133-
calendar->Release();
134-
return result;
122+
123+
const int num_bytes_in_utf8 = ::WideCharToMultiByte(
124+
CP_UTF8, 0, reinterpret_cast<const wchar_t*>(buffer),
125+
static_cast<int>(num_chars_in_buffer), nullptr, 0, nullptr, nullptr);
126+
std::string local_time_str;
127+
local_time_str.resize(static_cast<size_t>(num_bytes_in_utf8));
128+
::WideCharToMultiByte(CP_UTF8, 0, reinterpret_cast<const wchar_t*>(buffer),
129+
static_cast<int>(num_chars_in_buffer),
130+
&local_time_str[0], num_bytes_in_utf8, nullptr,
131+
nullptr);
132+
return local_time_str;
135133
}
136-
#endif
134+
#endif // USE_WIN32_LOCAL_TIME_ZONE
137135
} // namespace
138136

139137
std::string time_zone::name() const {
@@ -255,36 +253,9 @@ time_zone local_time_zone() {
255253
}
256254
#endif
257255
#if defined(USE_WIN32_LOCAL_TIME_ZONE)
258-
// Use the WinRT Calendar class to get the local time zone. This feature is
259-
// available on Windows 10 and later. The library is dynamically linked to
260-
// maintain binary compatibility with Windows XP - Windows 7. On Windows 8,
261-
// The combase.dll API functions are available but the RoActivateInstance
262-
// call will fail for the Calendar class.
263-
std::string winrt_tz;
264-
const HMODULE combase =
265-
LoadLibraryEx(_T("combase.dll"), nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
266-
if (combase) {
267-
const auto ro_initialize = reinterpret_cast<decltype(&::RoInitialize)>(
268-
GetProcAddress(combase, "RoInitialize"));
269-
const auto ro_uninitialize = reinterpret_cast<decltype(&::RoUninitialize)>(
270-
GetProcAddress(combase, "RoUninitialize"));
271-
if (ro_initialize && ro_uninitialize) {
272-
const HRESULT hr = ro_initialize(RO_INIT_MULTITHREADED);
273-
// RPC_E_CHANGED_MODE means that a previous RoInitialize call specified
274-
// a different concurrency model. The WinRT runtime is initialized and
275-
// should work for our purpose here, but we should *not* call
276-
// RoUninitialize because it's a failure.
277-
if (SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE) {
278-
winrt_tz = win32_local_time_zone(combase);
279-
if (SUCCEEDED(hr)) {
280-
ro_uninitialize();
281-
}
282-
}
283-
}
284-
FreeLibrary(combase);
285-
}
286-
if (!winrt_tz.empty()) {
287-
zone = winrt_tz.c_str();
256+
std::string win32_tz = win32_local_time_zone();
257+
if (!win32_tz.empty()) {
258+
zone = win32_tz.c_str();
288259
}
289260
#endif
290261

0 commit comments

Comments
 (0)