Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ cc_library(
"src/time_zone_posix.h",
"src/tzfile.h",
"src/zone_info_source.cc",
],
] + select({
"@platforms//os:windows": [
"src/time_zone_name_win.cc",
"src/time_zone_name_win.h",
],
"//conditions:default": [],
}),
hdrs = [
"include/cctz/time_zone.h",
"include/cctz/zone_info_source.h",
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ add_library(cctz
src/time_zone_posix.h
src/tzfile.h
src/zone_info_source.cc
$<$<PLATFORM_ID:Windows>:src/time_zone_name_win.cc>
$<$<PLATFORM_ID:Windows>:src/time_zone_name_win.h>
${CCTZ_HDRS}
)
cctz_target_set_cxx_standard(cctz)
Expand Down
108 changes: 6 additions & 102 deletions src/time_zone_lookup.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,6 @@
#include <zircon/types.h>
#endif

#if defined(_WIN32)
// Include only when <icu.h> is available.
// https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu-
// https://devblogs.microsoft.com/oldnewthing/20210527-00/?p=105255
#if defined(__has_include)
#if __has_include(<icu.h>)
#define USE_WIN32_LOCAL_TIME_ZONE
#include <windows.h>
#pragma push_macro("_WIN32_WINNT")
#pragma push_macro("NTDDI_VERSION")
// Minimum _WIN32_WINNT and NTDDI_VERSION to use ucal_getTimeZoneIDForWindowsID
#undef _WIN32_WINNT
#define _WIN32_WINNT 0x0A00 // == _WIN32_WINNT_WIN10
#undef NTDDI_VERSION
#define NTDDI_VERSION 0x0A000004 // == NTDDI_WIN10_RS3
#include <icu.h>
#pragma pop_macro("NTDDI_VERSION")
#pragma pop_macro("_WIN32_WINNT")
#include <timezoneapi.h>
#include <atomic>
#endif // __has_include(<icu.h>)
#endif // __has_include
#endif // _WIN32

#include <array>
#include <cstdint>
#include <cstdlib>
Expand All @@ -63,83 +39,11 @@
#include "time_zone_fixed.h"
#include "time_zone_impl.h"

namespace cctz {

namespace {
#if defined(USE_WIN32_LOCAL_TIME_ZONE)
// True if we have already failed to load the API.
static std::atomic_bool g_ucal_getTimeZoneIDForWindowsIDUnavailable;
static std::atomic<decltype(ucal_getTimeZoneIDForWindowsID)*>
g_ucal_getTimeZoneIDForWindowsIDRef;

std::string win32_local_time_zone() {
// If we have already failed to load the API, then just give up.
if (g_ucal_getTimeZoneIDForWindowsIDUnavailable.load()) {
return "";
}

auto ucal_getTimeZoneIDForWindowsIDFunc =
g_ucal_getTimeZoneIDForWindowsIDRef.load();
if (ucal_getTimeZoneIDForWindowsIDFunc == nullptr) {
// If we have already failed to load the API, then just give up.
if (g_ucal_getTimeZoneIDForWindowsIDUnavailable.load()) {
return "";
}

const HMODULE icudll = ::LoadLibraryExW(L"icu.dll", nullptr,
LOAD_LIBRARY_SEARCH_SYSTEM32);

if (icudll == nullptr) {
g_ucal_getTimeZoneIDForWindowsIDUnavailable.store(true);
return "";
}

ucal_getTimeZoneIDForWindowsIDFunc =
reinterpret_cast<decltype(ucal_getTimeZoneIDForWindowsID)*>(
::GetProcAddress(icudll, "ucal_getTimeZoneIDForWindowsID"));

if (ucal_getTimeZoneIDForWindowsIDFunc == nullptr) {
g_ucal_getTimeZoneIDForWindowsIDUnavailable.store(true);
return "";
}
// store-race is not a problem here, because ::GetProcAddress() returns the
// same address for the same function in the same DLL.
g_ucal_getTimeZoneIDForWindowsIDRef.store(
ucal_getTimeZoneIDForWindowsIDFunc);

// We intentionally do not call ::FreeLibrary() here to avoid frequent DLL
// loadings and unloading. As "icu.dll" is a system library, keeping it on
// memory is supposed to have no major drawback.
}

DYNAMIC_TIME_ZONE_INFORMATION info = {};
if (::GetDynamicTimeZoneInformation(&info) == TIME_ZONE_ID_INVALID) {
return "";
}

std::array<UChar, 128> buffer;
UErrorCode status = U_ZERO_ERROR;
const auto num_chars_in_buffer = ucal_getTimeZoneIDForWindowsIDFunc(
reinterpret_cast<const UChar*>(info.TimeZoneKeyName), -1, nullptr,
buffer.data(), static_cast<int32_t>(buffer.size()), &status);
if (status != U_ZERO_ERROR || num_chars_in_buffer <= 0 ||
num_chars_in_buffer > static_cast<int32_t>(buffer.size())) {
return "";
}
#if defined(_WIN32)
#include "time_zone_name_win.h"
#endif // _WIN32

const int num_bytes_in_utf8 = ::WideCharToMultiByte(
CP_UTF8, 0, reinterpret_cast<const wchar_t*>(buffer.data()),
static_cast<int>(num_chars_in_buffer), nullptr, 0, nullptr, nullptr);
std::string local_time_str;
local_time_str.resize(static_cast<size_t>(num_bytes_in_utf8));
::WideCharToMultiByte(CP_UTF8, 0, reinterpret_cast<const wchar_t*>(buffer.data()),
static_cast<int>(num_chars_in_buffer),
&local_time_str[0], num_bytes_in_utf8, nullptr,
nullptr);
return local_time_str;
}
#endif // USE_WIN32_LOCAL_TIME_ZONE
} // namespace
namespace cctz {

std::string time_zone::name() const {
return effective_impl().Name();
Expand Down Expand Up @@ -259,8 +163,8 @@ time_zone local_time_zone() {
zone = primary_tz.c_str();
}
#endif
#if defined(USE_WIN32_LOCAL_TIME_ZONE)
std::string win32_tz = win32_local_time_zone();
#if defined(_WIN32)
std::string win32_tz = GetWindowsLocalTimeZone();
if (!win32_tz.empty()) {
zone = win32_tz.c_str();
}
Expand Down
178 changes: 178 additions & 0 deletions src/time_zone_name_win.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "time_zone_name_win.h"

#if !defined(NOMINMAX)
#define NOMINMAX
#endif // !defined(NOMINMAX)
#include <windows.h>

#include <algorithm>
#include <atomic>
#include <cstdint>
#include <limits>
#include <string>
#include <type_traits>
#include <utility>

namespace cctz {
namespace {

// Define UChar as wchar_t here because Win32 APIs receive UTF-16 strings as
// wchar_t* instead of char16_t*. Using char16_t would require additional casts.
using UChar = wchar_t;

enum UErrorCode : std::int32_t {
U_ZERO_ERROR = 0,
U_BUFFER_OVERFLOW_ERROR = 15,
};

bool U_SUCCESS(UErrorCode error) { return error <= U_ZERO_ERROR; }

using ucal_getTimeZoneIDForWindowsID_func = std::int32_t(__cdecl*)(
const UChar* winid, std::int32_t len, const char* region, UChar* id,
std::int32_t id_capacity, UErrorCode* status);

std::atomic<bool> g_unavailable;
std::atomic<ucal_getTimeZoneIDForWindowsID_func>
g_ucal_getTimeZoneIDForWindowsID;

template <typename T> static T AsProcAddress(HMODULE module, const char* name) {
static_assert(
std::is_pointer<T>::value &&
std::is_function<typename std::remove_pointer<T>::type>::value,
"T must be a function pointer type");
const auto proc_address = ::GetProcAddress(module, name);
return reinterpret_cast<T>(reinterpret_cast<void*>(proc_address));
}

std::wstring GetSystem32Dir() {
std::wstring result;
std::uint32_t len = std::max<std::uint32_t>(
static_cast<std::uint32_t>(std::min<size_t>(
result.capacity(), std::numeric_limits<std::uint32_t>::max())),
1);
do {
result.resize(len);
len = ::GetSystemDirectoryW(&result[0], len);
} while (len > result.size());
result.resize(len);
return result;
}

ucal_getTimeZoneIDForWindowsID_func LoadIcuGetTimeZoneIDForWindowsID() {
// This function is intended to be lock free to avoid potential deadlocks
// with loader-lock taken inside LoadLibraryW. As LoadLibraryW and
Comment on lines +76 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended to be lock-free so this is a kind of feature. Added a comment at the beginning of the function.

I don't know why "lock-free" is a goal here. What deadlocks are possible if we only call LoadLibraryW() at most once?

That is, for my money, GetWindowsLocalTimeZone() should just say something like ...

  std::string local_time_zone;
  static const auto getTimeZoneIDForWindowsID = LoadIcuGetTimeZoneIDForWindowsID();
  if (getTimeZoneIDForWindowsID != nullptr) {
    // Set local_time_zone.
    ...
  }
  return local_time_zone;

No need for atomic anything.

Copy link
Contributor Author

@yukawa yukawa Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why "lock-free" is a goal here. What deadlocks are possible if we only call LoadLibraryW() at most once?

Win32 is an interesting environment, where things like LoadLibraryW(), GetProcAddress(), and DllMain() share the same lock.

As explained in #316 (comment), I guess deadlocks are still possible in the following scenario.

Suppose there are two running thread X and thread Y.

  1. Thread X enters DllMain, where the operating system implicitly acquires Loader lock.
  2. Thread Y acquires the lock to initialize static const auto getTimeZoneIDForWindowsID variable. It then get blocked at LoadLibraryW because the thread X still holds the Loader Lock.
  3. Thread X also tries to read static const auto getTimeZoneIDForWindowsID then gets blocked because it is not yet fully initialized.

The current approach with std::atomic_* doesn't have the above dead lock issue, as it is just an optimistic optimization that does not try to enforce the exactly-once initialization.

Let me know if I misunderstood your question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@devbww Let me know if there is any remaining concerns on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my question boils down to situation 3. Why/how would a thread inside DllMain be calling CCTZ's GetWindowsLocalTimeZone()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why/how would a thread inside DllMain be calling CCTZ's GetWindowsLocalTimeZone()?

In my observation, people do not directly call cctz::local_time_zone() but end up calling it through higher level libraries that internally use CCTZ. A most notable example would be abseil and protobuf.

For instance, just calling absl::InitializeLog() ends up internally calling GetWindowsLocalTimeZone().

https://github.com/abseil/abseil-cpp/blob/ff4395895672c03fb544be00e3d66ebada1fcc47/absl/log/initialize.cc#L35

void InitializeLog() { InitializeLogImpl(absl::LocalTimeZone()); }

Other commonly-used methods that internally trigger GetWindowsLocalTimeZone() would be:

  • absl::AbslStringify(absl::Time)
  • absl::FormatTime(absl::Time)

In the real world DllMain implementations, it becomes extremely difficult for us to see if methods like absl::InitializeLog() and absl::FormatTime(absl::Time) are indirectly called or not. Here is an excerpt from Chromium code base.

https://github.com/chromium/chromium/blob/c56a646a9557ea1d54bc4ab7b9be2f1320a47e2d/chrome/credential_provider/gaiacp/gaia_credential_provider_module.cc#L148-L204

BOOL CGaiaCredentialProviderModule::DllMain(HINSTANCE /*hinstance*/,
                                            DWORD reason,
                                            LPVOID reserved) {
  switch (reason) {
    case DLL_PROCESS_ATTACH: {
      exit_manager_ = std::make_unique<base::AtExitManager>();

      _set_invalid_parameter_handler(InvalidParameterHandler);

      // Initialize base.  Command line will be set from GetCommandLineW().
      base::CommandLine::Init(0, nullptr);

      // Initialize logging.
      logging::LoggingSettings settings;
      settings.logging_dest = logging::LOG_NONE;

      std::wstring log_file_path =
          GetGlobalFlagOrDefault(kRegLogFilePath, std::wstring{});
      if (not log_file_path.empty()) {
        settings.logging_dest = logging::LOG_TO_FILE;
        bool append_log = GetGlobalFlagOrDefault(kRegLogFileAppend, 0);
        settings.delete_old = append_log ? logging::APPEND_TO_OLD_LOG_FILE
                                         : logging::DELETE_OLD_LOG_FILE;
        settings.log_file_path = log_file_path;
      }

      logging::InitLogging(settings);
      logging::SetLogItems(true,    // Enable process id.
                           true,    // Enable thread id.
                           true,    // Enable timestamp.
                           false);  // Enable tickcount.
      logging::SetEventSource("GCPW", GCPW_CATEGORY, MSG_LOG_MESSAGE);
      if (GetGlobalFlagOrDefault(kRegEnableVerboseLogging, 0))
        logging::SetMinLogLevel(logging::LOGGING_VERBOSE);
      break;
    }
    case DLL_PROCESS_DETACH:
      LOGFN(VERBOSE) << "DllMain(DLL_PROCESS_DETACH)";

      // When this DLL is loaded for testing, don't reset the command line
      // since it causes tests to crash.
      if (!is_testing_)
        base::CommandLine::Reset();

      _set_invalid_parameter_handler(nullptr);
      exit_manager_.reset();

      crash_reporter::DestroyCrashpadClient();
      break;

    default:
      break;
  }

  return ATL::CAtlDllModuleT<CGaiaCredentialProviderModule>::DllMain(reason,
                                                                     reserved);
}

I actually don't know whether methods like absl::FormatTime(absl::Time) and absl::FormatTime(absl::Time) are called or not in the above code, but the fact it's hard to see is already a good reason to make it safe in the first place, I believe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm going to defer to your expertise here.

Still, it seems to me that it is a fundamental flaw having interfaces that take a lock, while having others that can call out to arbitrary code while holding the same lock. If that didn't happen everyone would be better off, and we wouldn't have to perform these dances.

// GetProcAddress are idempotent unless the DLL is unloaded, we just need to
// make sure global variables are read/written atomically, where
// memory_order_relaxed is also acceptable.

if (g_unavailable.load(std::memory_order_relaxed)) {
return nullptr;
}

{
const auto ucal_getTimeZoneIDForWindowsIDRef =
g_ucal_getTimeZoneIDForWindowsID.load(std::memory_order_relaxed);
if (ucal_getTimeZoneIDForWindowsIDRef != nullptr) {
return ucal_getTimeZoneIDForWindowsIDRef;
}
}

const std::wstring system32_dir = GetSystem32Dir();
if (system32_dir.empty()) {
g_unavailable.store(true, std::memory_order_relaxed);
return nullptr;
}

// Here LoadLibraryExW(L"icu.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) does
// not work if "icu.dll" is already loaded from somewhere other than the
// system32 directory. Specifying the full path with LoadLibraryW is more
// reliable.
const std::wstring icu_dll_path = system32_dir + L"\\icu.dll";
const HMODULE icu_dll = ::LoadLibraryW(icu_dll_path.c_str());
if (icu_dll == nullptr) {
g_unavailable.store(true, std::memory_order_relaxed);
return nullptr;
}

const auto ucal_getTimeZoneIDForWindowsIDRef =
AsProcAddress<ucal_getTimeZoneIDForWindowsID_func>(
icu_dll, "ucal_getTimeZoneIDForWindowsID");
if (ucal_getTimeZoneIDForWindowsIDRef != nullptr) {
g_unavailable.store(true, std::memory_order_relaxed);
return nullptr;
}

g_ucal_getTimeZoneIDForWindowsID.store(ucal_getTimeZoneIDForWindowsIDRef,
std::memory_order_relaxed);

return ucal_getTimeZoneIDForWindowsIDRef;
}

// Convert wchar_t array (UTF-16) to UTF-8 string
std::string Utf16ToUtf8(const wchar_t* ptr, size_t size) {
if (size > std::numeric_limits<int>::max()) {
return std::string();
}
const int chars_len = static_cast<int>(size);
std::string result;
std::int32_t len = std::max<std::int32_t>(
static_cast<std::int32_t>(std::min<size_t>(
result.capacity(), std::numeric_limits<std::int32_t>::max())),
1);
do {
result.resize(len);
// TODO: Switch to std::string::data() when we require C++17 or higher.
len = ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, ptr, chars_len,
&result[0], len, nullptr, nullptr);
} while (len > result.size());
result.resize(len);
return result;
}

} // namespace

std::string GetWindowsLocalTimeZone() {
const auto getTimeZoneIDForWindowsID = LoadIcuGetTimeZoneIDForWindowsID();
if (getTimeZoneIDForWindowsID == nullptr) {
return std::string();
}

DYNAMIC_TIME_ZONE_INFORMATION info = {};
if (::GetDynamicTimeZoneInformation(&info) == TIME_ZONE_ID_INVALID) {
return std::string();
}

std::wstring result;
std::int32_t len = std::max<std::int32_t>(
static_cast<std::int32_t>(std::min<size_t>(
result.capacity(), std::numeric_limits<std::int32_t>::max())),
1);
for (;;) {
UErrorCode status = U_ZERO_ERROR;
result.resize(len);
len = getTimeZoneIDForWindowsID(info.TimeZoneKeyName, -1, nullptr,
&result[0], len, &status);
if (U_SUCCESS(status)) {
return Utf16ToUtf8(result.data(), len);
}
if (status != U_BUFFER_OVERFLOW_ERROR) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably also fail if len != static_cast<std::int32_t>(len), as that would cause us to pass the wrong length on the next loop.

Indeed, that test should come before the getTimeZoneIDForWindowsID() call in case result.capacity() is initially beyond std::int32_t limits. That's highly unlikely, I know, but why not get it exactly right anyway.

Alternatively, the initial len could be capped at the maximum std::int32_t.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Initialized len with std::int32_t as the maximum.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that using &result[0] (here and elsewhere) while claiming to support C++11 still requires us to bound the initial len from below at 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return std::string();
}
}
}

} // namespace cctz
29 changes: 29 additions & 0 deletions src/time_zone_name_win.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef CCTZ_TIME_ZONE_NAME_WIN_H_
#define CCTZ_TIME_ZONE_NAME_WIN_H_

#include <string>

namespace cctz {

// Returns the local time zone ID in IANA format (e.g. "America/Los_Angeles"),
// or the empty string on failure. Not supported on Windows 10 1809 and earlier,
// where "icu.dll" is not available in the System32 directory.
std::string GetWindowsLocalTimeZone();

} // namespace cctz

#endif // CCTZ_TIME_ZONE_NAME_WIN_H_