diff --git a/CMakeLists.txt b/CMakeLists.txt index e4d26085..623ee658 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,7 +71,7 @@ if(WIN32) # resolve to the same *W types minwin.h forward-declares (otherwise the two # disagree and MSVC reports "redefinition; different basic types"). target_compile_definitions(eq_base PUBLIC OS_WIN NOMINMAX UNICODE _UNICODE) - target_link_libraries(eq_base PRIVATE psapi) + target_link_libraries(eq_base PRIVATE psapi synchronization) if(MSVC) target_compile_options(eq_base PUBLIC "/utf-8") endif() diff --git a/base/algorithm.h b/base/algorithm.h index 03515fb4..811ddfaf 100644 --- a/base/algorithm.h +++ b/base/algorithm.h @@ -97,7 +97,7 @@ inline mem_size CountIf(Iter first, Iter last, Pred pred) { return n; } -// ── RemoveIf (unstable - swaps to end) ────────────────────────────── +// ── RemoveIf (keeps the order of what it keeps, like std::remove_if) ── template inline Iter RemoveIf(Iter first, Iter last, Pred pred) { Iter result = first; @@ -303,4 +303,37 @@ inline T* LowerBound(T* first, T* last, const T& value) { return first; } +// First element for which comp(element, value) is false: std::lower_bound +// with a comparator, which may compare an element against a different type +// (a record against its key). +template +inline T* LowerBound(T* first, T* last, const V& value, Comp comp) { + while (first < last) { + T* mid = first + (last - first) / 2; + if (comp(*mid, value)) + first = mid + 1; + else + last = mid; + } + return first; +} + +// First element for which comp(value, element) is true: std::upper_bound. +template +inline T* UpperBound(T* first, T* last, const V& value, Comp comp) { + while (first < last) { + T* mid = first + (last - first) / 2; + if (!comp(value, *mid)) + first = mid + 1; + else + last = mid; + } + return first; +} + +template +inline T* UpperBound(T* first, T* last, const T& value) { + return UpperBound(first, last, value, [](const T& a, const T& b) { return a < b; }); +} + } // namespace base diff --git a/base/algorithm_test.cc b/base/algorithm_test.cc index ad19bd68..bddda19c 100644 --- a/base/algorithm_test.cc +++ b/base/algorithm_test.cc @@ -57,3 +57,29 @@ TEST(AlgorithmTest, SortOrdersAscending) { } } // namespace + +namespace { +struct Keyed { + i32 key; + i32 payload; +}; + +TEST(AlgorithmBounds, LowerAndUpperBoundWithComparator) { + Keyed items[] = {{1, 0}, {3, 1}, {3, 2}, {3, 3}, {7, 4}}; + auto before = [](const Keyed& k, i32 v) { return k.key < v; }; + auto after = [](i32 v, const Keyed& k) { return v < k.key; }; + EXPECT_EQ(base::LowerBound(items, items + 5, 3, before), items + 1); + EXPECT_EQ(base::UpperBound(items, items + 5, 3, after), items + 4); + EXPECT_EQ(base::LowerBound(items, items + 5, 0, before), items); + EXPECT_EQ(base::LowerBound(items, items + 5, 9, before), items + 5); + EXPECT_EQ(base::UpperBound(items, items + 5, 7, after), items + 5); + EXPECT_EQ(base::LowerBound(items, items, 3, before), items); +} + +TEST(AlgorithmBounds, UpperBoundPlain) { + i32 v[] = {1, 2, 2, 2, 5}; + EXPECT_EQ(base::UpperBound(v, v + 5, 2), v + 4); + EXPECT_EQ(base::UpperBound(v, v + 5, 0), v); + EXPECT_EQ(base::UpperBound(v, v + 5, 5), v + 5); +} +} // namespace diff --git a/base/containers/builtins_bit.h b/base/containers/builtins_bit.h index f75a9801..fe973243 100644 --- a/base/containers/builtins_bit.h +++ b/base/containers/builtins_bit.h @@ -1,112 +1,111 @@ -// Copyright (C) 2022 Vincent Hengel. -// For licensing information see LICENSE at the root of this distribution. -#pragma once - -#include -#include - -#if defined(_MSC_VER) && !defined(__clang__) -#include -#endif - -namespace base { - -#if defined(_MSC_VER) -template -[[nodiscard]] mem_size PopCount(const T value) noexcept { - constexpr mem_size _Digits = base::MinMax::digits(); - if constexpr (_Digits <= 16) { - return static_cast(__popcnt16(value)); - } else if constexpr (_Digits == 32) { - return static_cast(__popcnt(value)); - } else { -#ifdef _M_IX86 - return static_cast(__popcnt(value >> 32) + - __popcnt(static_cast(value))); -#else // ^^^ _M_IX86 / !_M_IX86 vvv - return static_cast(__popcnt64(value)); -#endif // _M_IX86 - } -} -#endif - -#if defined(OS_POSIX) -template -[[nodiscard]] mem_size PopCount(const T value) noexcept { - constexpr mem_size _Digits = base::MinMax::digits(); - if constexpr (_Digits <= 16) { - return static_cast(__builtin_popcount(static_cast(value))); - } else if constexpr (_Digits == 32) { - return static_cast(__builtin_popcount(value)); - } else { -#ifdef _M_IX86 - return static_cast(__builtin_popcount(value >> 32) + - __builtin_popcount(static_cast(value))); -#else // ^^^ _M_IX86 / !_M_IX86 vvv - return static_cast(__builtin_popcountll(value)); -#endif // _M_IX86 - } -} -#endif - -// Count of leading zero bits in |value|, as std::countl_zero would give. The -// builtins are undefined for zero, so that case is answered from the width. -template -[[nodiscard]] constexpr int CountLeftZero(const T value) noexcept { - // Width from sizeof, not MinMax::digits(): digits() follows - // std::numeric_limits and drops the sign bit, which is one short of the - // bit count these builtins actually operate on. - static_assert(base::is_integral_v && !base::is_signed_v, - "bit counting is defined for unsigned integers only"); - constexpr int kDigits = static_cast(sizeof(T) * 8); - if (value == 0) - return kDigits; -#if defined(_MSC_VER) && !defined(__clang__) - unsigned long index = 0; - if constexpr (kDigits <= 32) { - ::_BitScanReverse(&index, static_cast(value)); - return kDigits - 1 - static_cast(index); - } else { - ::_BitScanReverse64(&index, static_cast(value)); - return kDigits - 1 - static_cast(index); - } -#else - if constexpr (kDigits <= 32) { - // __builtin_clz works on a 32-bit unsigned; narrower types are widened, - // so subtract the padding the widening introduced. - return __builtin_clz(static_cast(value)) - (32 - kDigits); - } else { - return __builtin_clzll(static_cast(value)); - } -#endif -} - -// Count of trailing zero bits in |value|, as std::countr_zero would give. -template -[[nodiscard]] constexpr int CountRightZero(const T value) noexcept { - // Width from sizeof, not MinMax::digits(): digits() follows - // std::numeric_limits and drops the sign bit, which is one short of the - // bit count these builtins actually operate on. - static_assert(base::is_integral_v && !base::is_signed_v, - "bit counting is defined for unsigned integers only"); - constexpr int kDigits = static_cast(sizeof(T) * 8); - if (value == 0) - return kDigits; -#if defined(_MSC_VER) && !defined(__clang__) - unsigned long index = 0; - if constexpr (kDigits <= 32) { - ::_BitScanForward(&index, static_cast(value)); - } else { - ::_BitScanForward64(&index, static_cast(value)); - } - return static_cast(index); -#else - if constexpr (kDigits <= 32) { - return __builtin_ctz(static_cast(value)); - } else { - return __builtin_ctzll(static_cast(value)); - } -#endif -} - -} // namespace base +// Copyright (C) 2022 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +#include +#include + +#if defined(_MSC_VER) && !defined(__clang__) +#include +#endif + +namespace base { + +#if defined(_MSC_VER) +template +[[nodiscard]] mem_size PopCount(const T value) noexcept { + constexpr mem_size _Digits = base::MinMax::digits(); + if constexpr (_Digits <= 16) { + return static_cast(__popcnt16(value)); + } else if constexpr (_Digits == 32) { + return static_cast(__popcnt(value)); + } else { +#ifdef _M_IX86 + return static_cast(__popcnt(value >> 32) + + __popcnt(static_cast(value))); +#else // ^^^ _M_IX86 / !_M_IX86 vvv + return static_cast(__popcnt64(value)); +#endif // _M_IX86 + } +} +#else +// Every non-MSVC compiler, mingw-w64 included, which is not OS_POSIX. +template +[[nodiscard]] mem_size PopCount(const T value) noexcept { + constexpr mem_size _Digits = base::MinMax::digits(); + if constexpr (_Digits <= 16) { + return static_cast(__builtin_popcount(static_cast(value))); + } else if constexpr (_Digits == 32) { + return static_cast(__builtin_popcount(value)); + } else { +#ifdef _M_IX86 + return static_cast(__builtin_popcount(value >> 32) + + __builtin_popcount(static_cast(value))); +#else // ^^^ _M_IX86 / !_M_IX86 vvv + return static_cast(__builtin_popcountll(value)); +#endif // _M_IX86 + } +} +#endif + +// Count of leading zero bits in |value|, as std::countl_zero would give. The +// builtins are undefined for zero, so that case is answered from the width. +template +[[nodiscard]] constexpr int CountLeftZero(const T value) noexcept { + // Width from sizeof, not MinMax::digits(): digits() follows + // std::numeric_limits and drops the sign bit, which is one short of the + // bit count these builtins actually operate on. + static_assert(base::is_integral_v && !base::is_signed_v, + "bit counting is defined for unsigned integers only"); + constexpr int kDigits = static_cast(sizeof(T) * 8); + if (value == 0) + return kDigits; +#if defined(_MSC_VER) && !defined(__clang__) + unsigned long index = 0; + if constexpr (kDigits <= 32) { + ::_BitScanReverse(&index, static_cast(value)); + return kDigits - 1 - static_cast(index); + } else { + ::_BitScanReverse64(&index, static_cast(value)); + return kDigits - 1 - static_cast(index); + } +#else + if constexpr (kDigits <= 32) { + // __builtin_clz works on a 32-bit unsigned; narrower types are widened, + // so subtract the padding the widening introduced. + return __builtin_clz(static_cast(value)) - (32 - kDigits); + } else { + return __builtin_clzll(static_cast(value)); + } +#endif +} + +// Count of trailing zero bits in |value|, as std::countr_zero would give. +template +[[nodiscard]] constexpr int CountRightZero(const T value) noexcept { + // Width from sizeof, not MinMax::digits(): digits() follows + // std::numeric_limits and drops the sign bit, which is one short of the + // bit count these builtins actually operate on. + static_assert(base::is_integral_v && !base::is_signed_v, + "bit counting is defined for unsigned integers only"); + constexpr int kDigits = static_cast(sizeof(T) * 8); + if (value == 0) + return kDigits; +#if defined(_MSC_VER) && !defined(__clang__) + unsigned long index = 0; + if constexpr (kDigits <= 32) { + ::_BitScanForward(&index, static_cast(value)); + } else { + ::_BitScanForward64(&index, static_cast(value)); + } + return static_cast(index); +#else + if constexpr (kDigits <= 32) { + return __builtin_ctz(static_cast(value)); + } else { + return __builtin_ctzll(static_cast(value)); + } +#endif +} + +} // namespace base diff --git a/base/containers/queue.h b/base/containers/queue.h index dc36fca2..2a0c8059 100644 --- a/base/containers/queue.h +++ b/base/containers/queue.h @@ -6,6 +6,7 @@ #include #include +#include namespace base { @@ -13,6 +14,7 @@ template class Queue { public: void push(const T& value) { deque_.push_back(value); } + void push(T&& value) { deque_.push_back(base::move(value)); } void pop() { deque_.pop_front(); } T& front() { return deque_.front(); } diff --git a/base/containers/queue_test.cc b/base/containers/queue_test.cc index 27a1b699..524f4d8e 100644 --- a/base/containers/queue_test.cc +++ b/base/containers/queue_test.cc @@ -3,6 +3,7 @@ #include #include "queue.h" +#include "base/memory/unique_pointer.h" namespace base { namespace { @@ -53,5 +54,17 @@ TEST(QueueTest, GrowsPastInitialCapacity) { EXPECT_TRUE(queue.empty()); } +TEST(QueueTest, PushMovesMoveOnlyElements) { + base::Queue> queue; + queue.push(base::MakeUnique(7)); + auto value = base::MakeUnique(8); + queue.push(base::move(value)); + EXPECT_FALSE(value); + ASSERT_EQ(queue.size(), 2u); + EXPECT_EQ(*queue.front(), 7); + queue.pop(); + EXPECT_EQ(*queue.front(), 8); +} + } // namespace } // namespace base diff --git a/base/containers/span.h b/base/containers/span.h index 6af2ef5c..089e7c6d 100644 --- a/base/containers/span.h +++ b/base/containers/span.h @@ -13,11 +13,15 @@ #include #include +#include namespace base { template class Span { public: + static constexpr mem_size npos = static_cast(-1); + + constexpr Span() noexcept : ptr_(nullptr), len_(0) {} explicit constexpr Span(const T* ptr, mem_size len) : ptr_(ptr), len_(len) {} // TODO: disallow creating a span from a span. @@ -30,11 +34,38 @@ class Span { constexpr Span(T (&a)[N]) noexcept // NOLINT(runtime/explicit) : Span(a, N) {} - const T* data() const noexcept { return ptr_; } + // Span -> Span, also from a temporary, which the container + // constructor above cannot bind. The array-pointer test admits added + // qualifiers only, never a derived-to-base step that would change stride. + template + requires(!base::is_same_v && base::is_convertible_v) + constexpr Span(const Span& other) noexcept // NOLINT(runtime/explicit) + : Span(other.data(), other.size()) {} + + // Elements are as mutable as T: a Span is a writable window (File reads + // into one), a Span is not. begin() has always handed out T*. + T* data() const noexcept { return const_cast(ptr_); } mem_size size() const noexcept { return len_; } mem_size length() const noexcept { return len_; } + mem_size size_bytes() const noexcept { return len_ * sizeof(T); } bool empty() const noexcept { return len_ == 0; } + // Out-of-range bounds are a programmer error, as in operator[]. + Span first(mem_size count) const noexcept { + BASE_DCHECK(count <= len_); + return Span(ptr_, count); + } + Span last(mem_size count) const noexcept { + BASE_DCHECK(count <= len_); + return Span(ptr_ + (len_ - count), count); + } + Span subspan(mem_size offset, mem_size count = npos) const noexcept { + BASE_DCHECK(offset <= len_); + if (count == npos) count = len_ - offset; + BASE_DCHECK(count <= len_ - offset); + return Span(ptr_ + offset, count); + } + #if 0 template T& operator=(const TRhs& rhs) requires HasContainerTraits { @@ -42,19 +73,19 @@ class Span { } #endif - inline BASE_CONSTEXPR_ND const T& operator[](mem_size index) const noexcept { + inline BASE_CONSTEXPR_ND T& operator[](mem_size index) const noexcept { BASE_DCHECK(index < len_); - return ptr_[index]; + return data()[index]; } - BASE_CONSTEXPR_ND const T& front() const noexcept { + BASE_CONSTEXPR_ND T& front() const noexcept { BASE_DCHECK(ptr_ && len_ > 0); - return *ptr_; + return *data(); } - BASE_CONSTEXPR_ND const T& back() const noexcept { + BASE_CONSTEXPR_ND T& back() const noexcept { BASE_DCHECK(ptr_ && len_ > 0); - return ptr_[len_ - 1]; + return data()[len_ - 1]; } // iterator to beginning @@ -74,6 +105,22 @@ class Span { mem_size len_; }; +namespace span_detail { +template +struct ElementOf; +template +struct ElementOf { + using type = T; +}; +} // namespace span_detail + +// base::Span(container) spans the container's element type, const when the +// container is. +template + requires HasContainerTraits +Span(TT&) -> Span(nullptr)->data())>::type>; + // adapter for containers. template auto MakeSpan(TContainer& container) diff --git a/base/containers/span_test.cc b/base/containers/span_test.cc index 8fdb76e0..3b6d6d17 100644 --- a/base/containers/span_test.cc +++ b/base/containers/span_test.cc @@ -19,4 +19,82 @@ TEST(SpanTest, ConstructFrom) { base::Span b(vec); EXPECT_EQ(b.size(), 10); } + +TEST(SpanTest, DefaultIsEmpty) { + base::Span s; + EXPECT_TRUE(s.empty()); + EXPECT_EQ(s.data(), nullptr); + EXPECT_EQ(s.begin(), s.end()); +} + +TEST(SpanTest, WritesThroughNonConstElement) { + i32 arr[3] = {1, 2, 3}; + base::Span s(arr); + s[1] = 20; + s.front() = 10; + s.back() = 30; + s.data()[0] += 1; + EXPECT_EQ(arr[0], 11); + EXPECT_EQ(arr[1], 20); + EXPECT_EQ(arr[2], 30); +} + +TEST(SpanTest, Slices) { + const i32 arr[5] = {0, 1, 2, 3, 4}; + base::Span s(arr); + EXPECT_EQ(s.size_bytes(), 5 * sizeof(i32)); + + auto head = s.first(2); + ASSERT_EQ(head.size(), 2u); + EXPECT_EQ(head[1], 1); + + auto tail = s.last(2); + ASSERT_EQ(tail.size(), 2u); + EXPECT_EQ(tail[0], 3); + + auto mid = s.subspan(1, 3); + ASSERT_EQ(mid.size(), 3u); + EXPECT_EQ(mid.front(), 1); + EXPECT_EQ(mid.back(), 3); + + auto rest = s.subspan(2); + ASSERT_EQ(rest.size(), 3u); + EXPECT_EQ(rest[0], 2); + + EXPECT_TRUE(s.subspan(5).empty()); + EXPECT_TRUE(s.first(0).empty()); +} + +TEST(SpanTest, ConstSpanFromMutableSpan) { + i32 arr[2] = {7, 8}; + base::Span writable(arr); + base::Span view(writable); + EXPECT_EQ(view.size(), 2u); + EXPECT_EQ(view[1], 8); +} + +TEST(SpanTest, ConstSpanFromTemporarySpan) { + i32 arr[3] = {1, 2, 3}; + auto sum = [](base::Span s) { + i32 total = 0; + for (i32 v : s) total += v; + return total; + }; + EXPECT_EQ(sum(base::Span(arr, 3)), 6); + EXPECT_EQ(sum(base::Span(&arr[1], 1)), 2); +} + +TEST(SpanTest, DeducesFromContainer) { + base::Vector vec; + vec.push_back(4); + vec.push_back(5); + auto s = base::Span(vec); + static_assert(base::is_same_v>); + EXPECT_EQ(s.first(1)[0], 4); + + const base::Vector& cvec = vec; + auto cs = base::Span(cvec); + static_assert(base::is_same_v>); + EXPECT_EQ(cs.size(), 2u); +} } // namespace diff --git a/base/containers/vector.h b/base/containers/vector.h index eb564504..7a86466a 100644 --- a/base/containers/vector.h +++ b/base/containers/vector.h @@ -189,10 +189,11 @@ class Vector { return *this; } - void resize(mem_size new_size, const value_type& value) { + void resize(mem_size new_size, const value_type& element) { const auto current_size = size(); if (new_size > current_size) { const auto count = new_size - current_size; + const value_type value(element); // may name an element; see insert if (new_size > capacity()) { GrowCapacity(current_size, new_size); } @@ -319,9 +320,12 @@ class Vector { } // single element at a specified position - T* insert(T* pos, const T& value) { + T* insert(T* pos, const T& element) { BASE_DCHECK(pos >= begin() && pos <= end(), "Vector::insert: Invalid position"); const auto index = pos - begin(); + // A copy first: growing or shifting would move an element of this vector + // out from under a reference to it. + const T value(element); if (size() == capacity()) { const mem_size new_cap = CalculateNewCapacity(size()); @@ -342,11 +346,12 @@ class Vector { } // Inserts multiple copies of an element - void insert(T* pos, mem_size count, const T& value) { + void insert(T* pos, mem_size count, const T& element) { if (count == 0) return; BASE_DCHECK(pos >= begin() && pos <= end(), "Vector::insert: Invalid position"); const auto index = pos - begin(); + const T value(element); // see insert(pos, value) const mem_size required = base::CheckedCountSum(size(), count); if (required > capacity()) { @@ -621,12 +626,26 @@ class Vector { } } + // Constructs the new element in the new block before the old ones move: + // args may name an element of this vector (v.push_back(v.back())), and + // growing first would leave them pointing at freed storage. template void InsertAtEnd(TArgs&&... args) { const auto current_size = size(); const auto new_cap = CalculateNewCapacity(current_size); - GrowCapacity(current_size, new_cap); - ::new (static_cast(end_++)) T(base::forward(args)...); + T* new_block = Vector::Allocate(new_cap); + ::new (static_cast(new_block + current_size)) T(base::forward(args)...); + if (data_) { + T* new_spot = new_block; + for (T* first = data_; first != end_; ++first, ++new_spot) { + ::new (reinterpret_cast(new_spot)) T(base::move(*first)); + } + base::DestructRange(data_, end_); + Vector::Free(data_, capacity()); + } + data_ = new_block; + end_ = new_block + current_size + 1; + capacity_ = new_block + new_cap; } void GrowCapacity(mem_size current_size, mem_size new_cap) { diff --git a/base/containers/vector_test.cc b/base/containers/vector_test.cc index dcf94dcb..ee35dc5e 100644 --- a/base/containers/vector_test.cc +++ b/base/containers/vector_test.cc @@ -9,6 +9,7 @@ #include #include +#include #include namespace { @@ -471,3 +472,34 @@ TEST(VectorOverflowDeathTest, InsertCountThatOverflowsTheSize) { } } // namespace + +namespace { +// std::vector lets an argument name one of its own elements; growing before +// reading it used to read freed storage. +TEST(VectorAliasing, PushBackOwnElementWhileGrowing) { + base::Vector v; + v.push_back("a fairly long string that lives on the heap, not inline"); + for (int i = 0; i < 40; ++i) { + v.push_back(v.back()); + v.emplace_back(v.front()); + } + for (const base::String& s : v) EXPECT_EQ(s, v[0]); + EXPECT_EQ(v.size(), 81u); +} + +TEST(VectorAliasing, InsertAndResizeOwnElement) { + base::Vector v; + v.push_back("first element, long enough to need a heap block"); + v.push_back("second element, also long enough to need a heap block"); + v.shrink_to_fit(); + v.insert(v.begin(), v[1]); + ASSERT_EQ(v.size(), 3u); + EXPECT_EQ(v[0], v[2]); + v.insert(v.begin() + 1, 2, v[0]); + ASSERT_EQ(v.size(), 5u); + EXPECT_EQ(v[1], v[4]); + EXPECT_EQ(v[2], v[4]); + v.resize(64, v[3]); + EXPECT_EQ(v[63], v[3]); +} +} // namespace diff --git a/base/filesystem/file_test.cc b/base/filesystem/file_test.cc index 9c9dd207..cb7cb087 100644 --- a/base/filesystem/file_test.cc +++ b/base/filesystem/file_test.cc @@ -6,6 +6,8 @@ #include #include +#include + #if 0 #include #include @@ -122,4 +124,26 @@ TEST(FileTests, Create) { EXPECT_FALSE(base::PathExists(file_path)); } #endif -} // namespace \ No newline at end of file +} // namespace + +namespace { +// GetInfo used to leave Info at its defaults on POSIX: FromStat was never +// filled in, so every file reported size 0. +TEST(FileTests, GetInfoReportsSizeAndKind) { + const base::Path file_path("file_getinfo_test.tmp"); + { + base::File file(file_path, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | + base::File::FLAG_READ); + ASSERT_TRUE(file.IsValid()); + const char kData[] = "0123456789"; + ASSERT_EQ(file.Write(0, kData, 10), 10); + + base::File::Info info; + ASSERT_TRUE(file.GetInfo(&info)); + EXPECT_EQ(info.size, 10); + EXPECT_FALSE(info.is_directory); + EXPECT_FALSE(info.is_symbolic_link); + } + ::remove("file_getinfo_test.tmp"); +} +} // namespace diff --git a/base/filesystem/path.cc b/base/filesystem/path.cc index 478a578b..3998ab6b 100644 --- a/base/filesystem/path.cc +++ b/base/filesystem/path.cc @@ -4,6 +4,8 @@ #include #include +#include + namespace base { namespace { diff --git a/base/filesystem/posix/file_posix.cc b/base/filesystem/posix/file_posix.cc index 8e6cc9fe..2f3594ad 100644 --- a/base/filesystem/posix/file_posix.cc +++ b/base/filesystem/posix/file_posix.cc @@ -92,8 +92,10 @@ File::Error CallFcntlFlock(PlatformFile file, File::LockMode* mode) { } // namespace -void File::Info::FromStat(const stat_wrapper_t& /*stat_info*/) { - BASE_IMPOSSIBLE; +void File::Info::FromStat(const stat_wrapper_t& stat_info) { + is_directory = S_ISDIR(stat_info.st_mode); + is_symbolic_link = S_ISLNK(stat_info.st_mode); + size = static_cast(stat_info.st_size); } bool File::IsValid() const { diff --git a/base/filesystem/posix/file_util_posix.cc b/base/filesystem/posix/file_util_posix.cc index 027612b6..abcdc0c7 100644 --- a/base/filesystem/posix/file_util_posix.cc +++ b/base/filesystem/posix/file_util_posix.cc @@ -8,6 +8,7 @@ #include #include #include // for O_NONBLOCK etc +#include // realpath, mkdtemp #include #include diff --git a/base/logging.cc b/base/logging.cc index 8caaf7aa..5732415c 100644 --- a/base/logging.cc +++ b/base/logging.cc @@ -6,6 +6,8 @@ #include #include +#include + namespace base { namespace { diff --git a/base/meta/traits.h b/base/meta/traits.h index e48b0d36..b9591d8d 100644 --- a/base/meta/traits.h +++ b/base/meta/traits.h @@ -193,4 +193,73 @@ struct underlying_type { template using underlying_type_t = typename underlying_type::type; +template +struct remove_cv { + using type = T; +}; +template +struct remove_cv { + using type = T; +}; +template +struct remove_cv { + using type = T; +}; +template +struct remove_cv { + using type = T; +}; +template +using remove_cv_t = typename remove_cv::type; + +namespace traits_detail { +template +struct strip_reference { + using type = T; +}; +template +struct strip_reference { + using type = T; +}; +template +struct strip_reference { + using type = T; +}; +} // namespace traits_detail + +template +struct remove_cvref { + using type = remove_cv_t::type>; +}; +template +using remove_cvref_t = typename remove_cvref::type; + +template +inline constexpr bool is_array_v = false; +template +inline constexpr bool is_array_v = true; +template +inline constexpr bool is_array_v = true; + +// Size of the first dimension, 0 for a non-array or an array of unknown bound. +template +inline constexpr decltype(sizeof(0)) extent_v = 0; +template +inline constexpr decltype(sizeof(0)) extent_v = N; + +// remove_extent lives in base/memory/cxx_lifetime.h. + +// The object-lifetime traits have no portable library-free spelling; these +// builtins are what GCC, Clang and MSVC's own are built on. +template +inline constexpr bool is_trivially_copyable_v = __is_trivially_copyable(T); +template +inline constexpr bool is_trivially_destructible_v = __has_trivial_destructor(T); +template +inline constexpr bool is_default_constructible_v = __is_constructible(T); +template +inline constexpr bool is_move_constructible_v = __is_constructible(T, T&&); +template +inline constexpr bool is_nothrow_move_constructible_v = __is_nothrow_constructible(T, T&&); + } // namespace base diff --git a/base/meta/traits_test.cc b/base/meta/traits_test.cc index 7718f9bb..5940f57e 100644 --- a/base/meta/traits_test.cc +++ b/base/meta/traits_test.cc @@ -4,6 +4,7 @@ #include #include +#include #include namespace { @@ -93,3 +94,34 @@ TEST(Traits, EvaluateAtCompileTime) { } } // namespace + +namespace { +struct Movable { + Movable(Movable&&) noexcept {} +}; +struct ThrowingMove { + ThrowingMove(ThrowingMove&&) {} +}; +struct NoDefault { + explicit NoDefault(int) {} +}; +struct NonTrivial { + ~NonTrivial() {} +}; + +static_assert(base::is_same_v, int>); +static_assert(base::is_same_v, int>); +static_assert(base::is_same_v, const int*>); +static_assert(base::is_array_v && base::is_array_v && !base::is_array_v); +static_assert(base::extent_v == 4 && base::extent_v == 0); +static_assert(base::is_same_v, float>); +static_assert(base::is_same_v, int[3]>); +static_assert(base::is_trivially_copyable_v && !base::is_trivially_copyable_v); +static_assert(base::is_trivially_destructible_v && !base::is_trivially_destructible_v); +static_assert(base::is_default_constructible_v && !base::is_default_constructible_v); +static_assert(base::is_move_constructible_v); +static_assert(base::is_nothrow_move_constructible_v); +static_assert(!base::is_nothrow_move_constructible_v); + +TEST(TraitsTest, ObjectTraitsCompile) { SUCCEED(); } +} // namespace diff --git a/base/premake5.lua b/base/premake5.lua index ae051c84..f66b2d42 100644 --- a/base/premake5.lua +++ b/base/premake5.lua @@ -20,7 +20,7 @@ local function base_project() filter("configurations:Profile") dependencies("tracysdk") filter("system:windows") - links("psapi") + links({"psapi", "synchronization"}) filter{} warnings("High") diff --git a/base/strings/base_string.h b/base/strings/base_string.h index 96b84cad..bbcd3f77 100644 --- a/base/strings/base_string.h +++ b/base/strings/base_string.h @@ -436,6 +436,16 @@ class BasicBaseString { character_type* begin() noexcept { return get_data(); } character_type* end() noexcept { return get_data() + get_size(); } + character_type& front() { + BASE_BUGCHECK(!empty(), "Cannot access .front() of an empty string"); + return get_data()[0]; + } + + const character_type& front() const { + BASE_BUGCHECK(!empty(), "Cannot access .front() of an empty string"); + return get_data()[0]; + } + character_type& back() { BASE_BUGCHECK(!empty(), "Cannot access .back() of an empty string"); return get_data()[get_size() - 1]; diff --git a/base/strings/base_string_test.cc b/base/strings/base_string_test.cc index d1472fa3..7463f037 100644 --- a/base/strings/base_string_test.cc +++ b/base/strings/base_string_test.cc @@ -616,6 +616,7 @@ TEST(BaseStringAssign, AssignsFromAnotherStringLike) { // Without the formatter specialization C++23 range formatting takes over and // prints ['a', 'b'], and a standard library without it fails to compile at all. +#if !defined(BASE_NO_STD_FORMAT) TEST(BaseStringFormat, FormatsAsTextNotAsARange) { const base::String s = "hello"; @@ -635,6 +636,7 @@ TEST(BaseStringFormat, FormatsAStringRef) { EXPECT_EQ(std::format("{}", ref), "world"); } +#endif // !BASE_NO_STD_FORMAT TEST(BaseStringViewInterop, ConvertsToAStdStringView) { base::String s = "meshes/rock.nif"; @@ -867,4 +869,15 @@ TEST(BaseStringOverflowDeathTest, WideStringReserveAboveMaxSizeTerminates) { "capacity exceeds max_size"); } -} // namespace \ No newline at end of file +} // namespace + +namespace { +TEST(BaseStringFront, ReadsAndWritesFirstCharacter) { + base::String s("abc"); + EXPECT_EQ(s.front(), 'a'); + s.front() = 'x'; + EXPECT_EQ(s, "xbc"); + const base::String& c = s; + EXPECT_EQ(c.front(), 'x'); +} +} // namespace diff --git a/base/strings/string_ref.h b/base/strings/string_ref.h index aa04b0d4..bf261ef7 100644 --- a/base/strings/string_ref.h +++ b/base/strings/string_ref.h @@ -14,6 +14,8 @@ #include #include +#include + namespace base { // template @@ -373,14 +375,48 @@ class BasicStringRef { return base::MemCompare(data_ + (length_ - s.size()), s.data(), s.size() * sizeof(TChar)) == 0; } + // The find_*_not_of family follows std::string_view (and base::String): + // the second argument is a position, never a set length. constexpr mem_size find_first_not_of(const TChar* s, mem_size pos, mem_size count) const { - return base::StringSearchNotOf(data_, length(), s, pos, count); + for (mem_size i = pos; i < length_; ++i) { + if (!ContainsChar(s, count, data_[i])) + return i; + } + return npos; + } + + constexpr mem_size find_first_not_of(const TChar* set, mem_size pos = 0) const { + if (!set) + return npos; + return find_first_not_of(set, pos, base::CountStringLength(set)); + } + + constexpr mem_size find_first_not_of(TChar c, mem_size pos = 0) const { + return find_first_not_of(&c, pos, 1); + } + + constexpr mem_size find_last_not_of(const TChar* s, mem_size pos, mem_size count) const { + if (length_ == 0) + return npos; + mem_size i = (pos == npos || pos >= length_) ? length_ - 1 : pos; + for (;; --i) { + if (!ContainsChar(s, count, data_[i])) + return i; + if (i == 0) + return npos; + } } - constexpr mem_size find_last_not_of(const TChar* s, mem_size count) const { - return base::StringSearchLastNotOf(data_, length(), s, count); + constexpr mem_size find_last_not_of(const TChar* set, mem_size pos = npos) const { + if (!set) + return npos; + return find_last_not_of(set, pos, base::CountStringLength(set)); + } + + constexpr mem_size find_last_not_of(TChar c, mem_size pos = npos) const { + return find_last_not_of(&c, pos, 1); } #if 0 @@ -422,6 +458,14 @@ class BasicStringRef { } private: + static constexpr bool ContainsChar(const TChar* set, mem_size count, TChar c) { + for (mem_size k = 0; k < count; ++k) { + if (set[k] == c) + return true; + } + return false; + } + static constexpr TChar s_empty_[1] = {0}; const TChar* data_; u32 length_; diff --git a/base/strings/string_ref_test.cc b/base/strings/string_ref_test.cc index c0049c20..f25f078f 100644 --- a/base/strings/string_ref_test.cc +++ b/base/strings/string_ref_test.cc @@ -187,4 +187,30 @@ TEST(BasicStringRefTest, Contains) { EXPECT_TRUE(ref.contains("world")); EXPECT_FALSE(ref.contains("moon")); } -} // namespace \ No newline at end of file +} // namespace +namespace { +// find_*_not_of take (set, pos) like std::string_view, never (set, length). +TEST(StringRefNotOf, FirstNotOf) { + const base::StringRef s(" \tab c "); + EXPECT_EQ(s.find_first_not_of(" \t"), 3u); + EXPECT_EQ(s.find_first_not_of(" \t", 4), 4u); + EXPECT_EQ(s.find_first_not_of(" \t", 5), 6u); + EXPECT_EQ(s.find_first_not_of(' '), 2u); + EXPECT_EQ(s.find_first_not_of(" \tabc"), base::StringRef::npos); + EXPECT_EQ(s.find_first_not_of("ab", 0, 0), 0u); +} + +TEST(StringRefNotOf, LastNotOf) { + const base::StringRef s(" \tab c "); + EXPECT_EQ(s.find_last_not_of(" \t"), 6u); + EXPECT_EQ(s.find_last_not_of(" \t", 5), 4u); + EXPECT_EQ(s.find_last_not_of(' '), 6u); + EXPECT_EQ(s.find_last_not_of(" \tabc"), base::StringRef::npos); +} + +TEST(StringRefNotOf, EmptyHaystack) { + const base::StringRef s; + EXPECT_EQ(s.find_first_not_of(" "), base::StringRef::npos); + EXPECT_EQ(s.find_last_not_of(" "), base::StringRef::npos); +} +} // namespace diff --git a/base/strings/string_search.h b/base/strings/string_search.h index b4425235..775cb7c1 100644 --- a/base/strings/string_search.h +++ b/base/strings/string_search.h @@ -4,6 +4,8 @@ #include #include +#include + namespace base { // a special size value, for indicating when we failed to find something static constexpr mem_size kStringNotFoundPos = mem_size(-1); diff --git a/base/threading/condition_variable.h b/base/threading/condition_variable.h new file mode 100644 index 00000000..b90485b6 --- /dev/null +++ b/base/threading/condition_variable.h @@ -0,0 +1,59 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +#include +#include +#include +#include +#include + +namespace base { + +// Sequence-counter condition variable over the platform's address wait +// (futex / __ulock / WaitOnAddress). Wait always loops on a predicate, so a +// spurious wakeup is harmless. Lock is anything with lock()/unlock(): a +// base::UniqueLock or the base::Mutex itself. +class BASE_EXPORT ConditionVariable { + public: + constexpr ConditionVariable() = default; + BASE_NOCOPYMOVE(ConditionVariable); + + // |lock| must be held on entry and is held again on return. + template + void Wait(Lock& lock, Pred pred) { + while (!pred()) { + // Read under the lock, before releasing it. A notifier that changes the + // predicate after this point must bump the counter after we read it, so + // WaitWhileEquals returns immediately instead of losing the wakeup. + const u32 seq = seq_.load(base::memory_order_relaxed); + lock.unlock(); + WaitWhileEquals(seq); + lock.lock(); + } + } + + // Blocks until a notify, until |timeout| has passed, or spuriously, like + // std::condition_variable::wait_for without a predicate: callers re-check + // their condition. |lock| must be held on entry and is held again on return. + template + void WaitFor(Lock& lock, TimeDelta timeout) { + const u32 seq = seq_.load(base::memory_order_relaxed); + lock.unlock(); + WaitWhileEqualsFor(seq, timeout); + lock.lock(); + } + + void NotifyOne(); + void NotifyAll(); + + private: + // Blocks while seq_ still equals |seq|. May return spuriously. + void WaitWhileEquals(u32 seq); + // As WaitWhileEquals, giving up after |timeout|. + void WaitWhileEqualsFor(u32 seq, TimeDelta timeout); + + base::Atomic seq_{0}; +}; + +} // namespace base diff --git a/base/threading/condition_variable_linux.cc b/base/threading/condition_variable_linux.cc new file mode 100644 index 00000000..8577b7cd --- /dev/null +++ b/base/threading/condition_variable_linux.cc @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include + +#include +#include +#include +#include + +#include +#include + +namespace base { + +void ConditionVariable::WaitWhileEquals(u32 seq) { + int saved_errno = errno; + syscall(SYS_futex, &seq_, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, seq, nullptr, + nullptr, 0); + errno = saved_errno; +} + +void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { + const i64 us = timeout.InMicroseconds(); + if (us <= 0) + return; + // FUTEX_WAIT takes a relative timeout. + struct timespec ts; + ts.tv_sec = static_cast(us / 1000000); + ts.tv_nsec = static_cast((us % 1000000) * 1000); + int saved_errno = errno; + syscall(SYS_futex, &seq_, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, seq, &ts, nullptr, + 0); + errno = saved_errno; +} + +void ConditionVariable::NotifyOne() { + seq_.fetch_add(1, base::memory_order_relaxed); + int saved_errno = errno; + syscall(SYS_futex, &seq_, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1, nullptr, + nullptr, 0); + errno = saved_errno; +} + +void ConditionVariable::NotifyAll() { + seq_.fetch_add(1, base::memory_order_relaxed); + int saved_errno = errno; + syscall(SYS_futex, &seq_, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, INT_MAX, nullptr, + nullptr, 0); + errno = saved_errno; +} + +} // namespace base diff --git a/base/threading/condition_variable_mac.cc b/base/threading/condition_variable_mac.cc new file mode 100644 index 00000000..34481d5d --- /dev/null +++ b/base/threading/condition_variable_mac.cc @@ -0,0 +1,47 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include + +#include + +namespace base { + +// Same __ulock contract spinning_mutex.cc uses: UL_COMPARE_AND_WAIT blocks +// while the 32-bit word still equals the passed value. +extern "C" int __ulock_wait(uint32_t operation, void* addr, uint64_t value, + uint32_t timeout_us); +extern "C" int __ulock_wake(uint32_t operation, void* addr, uint64_t wake_value); + +namespace { +constexpr uint32_t kUlCompareAndWait = 1; +constexpr uint32_t kUlfWakeAll = 0x00000100; +constexpr uint32_t kUlfNoErrno = 0x01000000; +} // namespace + +void ConditionVariable::WaitWhileEquals(u32 seq) { + __ulock_wait(kUlCompareAndWait | kUlfNoErrno, &seq_, seq, 0); +} + +void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { + const i64 us = timeout.InMicroseconds(); + if (us <= 0) + return; + // A timeout of 0 means forever to __ulock_wait, so the longest finite wait + // is UINT32_MAX microseconds; callers re-check and wait again past that. + const uint32_t timeout_us = + us > static_cast(UINT32_MAX) ? UINT32_MAX : static_cast(us); + __ulock_wait(kUlCompareAndWait | kUlfNoErrno, &seq_, seq, timeout_us); +} + +void ConditionVariable::NotifyOne() { + seq_.fetch_add(1, base::memory_order_relaxed); + __ulock_wake(kUlCompareAndWait | kUlfNoErrno, &seq_, 0); +} + +void ConditionVariable::NotifyAll() { + seq_.fetch_add(1, base::memory_order_relaxed); + __ulock_wake(kUlCompareAndWait | kUlfWakeAll | kUlfNoErrno, &seq_, 0); +} + +} // namespace base diff --git a/base/threading/condition_variable_test.cc b/base/threading/condition_variable_test.cc new file mode 100644 index 00000000..e06e5182 --- /dev/null +++ b/base/threading/condition_variable_test.cc @@ -0,0 +1,257 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +using Lock = base::UniqueLock; + +TEST(ConditionVariable, TruePredicateReturnsWithoutBlocking) { + base::Mutex mutex; + base::ConditionVariable cv; + Lock lock(mutex); + int calls = 0; + cv.Wait(lock, [&] { + calls++; + return true; + }); + EXPECT_EQ(calls, 1); + EXPECT_TRUE(lock.owns_lock()); +} + +TEST(ConditionVariable, WaitAcceptsTheMutexDirectly) { + base::Mutex mutex; + base::ConditionVariable cv; + bool ready = false; + + base::Thread producer( + "cv-direct", + [&] { + base::LockGuard guard(mutex); + ready = true; + cv.NotifyOne(); + }, + /*start_now=*/true); + + mutex.lock(); + cv.Wait(mutex, [&] { return ready; }); + mutex.unlock(); + ASSERT_TRUE(producer.Join()); +} + +TEST(ConditionVariable, ProducerConsumerHandoff) { + constexpr int kItems = 1000; + base::Mutex mutex; + base::ConditionVariable cv; + // One-slot mailbox: 0 means empty, so the producer waits for the consumer + // to drain each item and both sides block on every step. + int slot = 0; + long long sum = 0; + + base::Thread consumer( + "cv-consumer", + [&] { + for (int i = 0; i < kItems; i++) { + Lock lock(mutex); + cv.Wait(lock, [&] { return slot != 0; }); + sum += slot; + slot = 0; + cv.NotifyAll(); + } + }, + /*start_now=*/true); + + base::Thread producer( + "cv-producer", + [&] { + for (int i = 1; i <= kItems; i++) { + Lock lock(mutex); + cv.Wait(lock, [&] { return slot == 0; }); + slot = i; + cv.NotifyAll(); + } + }, + /*start_now=*/true); + + ASSERT_TRUE(producer.Join()); + ASSERT_TRUE(consumer.Join()); + EXPECT_EQ(sum, static_cast(kItems) * (kItems + 1) / 2); +} + +TEST(ConditionVariable, NotifyAllReleasesEveryWaiter) { + constexpr int kWaiters = 8; + base::Mutex mutex; + base::ConditionVariable cv; + bool go = false; + int waiting = 0; + base::Atomic released{0}; + base::Thread* threads[kWaiters]; + + for (int i = 0; i < kWaiters; i++) { + threads[i] = new base::Thread( + "cv-waiter", + [&] { + Lock lock(mutex); + waiting++; + cv.NotifyAll(); + cv.Wait(lock, [&] { return go; }); + released.fetch_add(1); + }, + /*start_now=*/true); + } + + { + Lock lock(mutex); + cv.Wait(lock, [&] { return waiting == kWaiters; }); + EXPECT_EQ(released.load(), 0); + go = true; + } + cv.NotifyAll(); + + for (int i = 0; i < kWaiters; i++) { + EXPECT_TRUE(threads[i]->Join()); + delete threads[i]; + } + EXPECT_EQ(released.load(), kWaiters); +} + +TEST(ConditionVariable, TurnTakingStress) { + // Threads take turns by ticket: each waits until the shared counter reaches + // its own residue, bumps it, and wakes the rest. Every step hands off to a + // blocked thread, so a lost wakeup shows up as a hang and a race as a + // miscount. + constexpr int kThreads = 4; + constexpr int kRounds = 5000; + base::Mutex mutex; + base::ConditionVariable cv; + int counter = 0; + base::Thread* threads[kThreads]; + + for (int t = 0; t < kThreads; t++) { + threads[t] = new base::Thread( + "cv-stress", + [&, t] { + for (int r = 0; r < kRounds; r++) { + Lock lock(mutex); + cv.Wait(lock, [&] { return counter % kThreads == t; }); + counter++; + // Not NotifyOne: it may wake a thread whose turn it is not. + cv.NotifyAll(); + } + }, + /*start_now=*/true); + } + for (int t = 0; t < kThreads; t++) { + EXPECT_TRUE(threads[t]->Join()); + delete threads[t]; + } + EXPECT_EQ(counter, kThreads * kRounds); +} + +TEST(ConditionVariable, NotifyOneCounterStress) { + // Producers add work under the lock and wake one consumer each; consumers + // drain it. Every item needs a wakeup, so a lost one leaves a consumer + // asleep with work pending and the join hangs. + constexpr int kProducers = 3; + constexpr int kConsumers = 3; + constexpr int kPerProducer = 20000; + constexpr int kTotal = kProducers * kPerProducer; + base::Mutex mutex; + base::ConditionVariable cv; + int pending = 0; + int consumed = 0; + base::Thread* threads[kProducers + kConsumers]; + + for (int c = 0; c < kConsumers; c++) { + threads[c] = new base::Thread( + "cv-consume", + [&] { + for (;;) { + Lock lock(mutex); + cv.Wait(lock, [&] { return pending > 0 || consumed == kTotal; }); + if (pending == 0) + return; + pending--; + if (++consumed == kTotal) + cv.NotifyAll(); + } + }, + /*start_now=*/true); + } + for (int p = 0; p < kProducers; p++) { + threads[kConsumers + p] = new base::Thread( + "cv-produce", + [&] { + for (int i = 0; i < kPerProducer; i++) { + { + Lock lock(mutex); + pending++; + } + // Outside the lock on purpose: the sequence counter must still + // catch a notify that lands between a waiter's check and its sleep. + cv.NotifyOne(); + } + }, + /*start_now=*/true); + } + for (auto* thread : threads) { + EXPECT_TRUE(thread->Join()); + delete thread; + } + EXPECT_EQ(consumed, kTotal); + EXPECT_EQ(pending, 0); +} + +TEST(ConditionVariable, WaitForTimesOutWithoutNotify) { + base::Mutex mutex; + base::ConditionVariable cv; + Lock lock(mutex); + const base::TimeTicks start = base::TimeTicks::Now(); + // Spurious wakes are allowed, so loop until the deadline like a caller would. + const base::TimeTicks deadline = start + base::Milliseconds(20); + while (base::TimeTicks::Now() < deadline) + cv.WaitFor(lock, deadline - base::TimeTicks::Now()); + EXPECT_TRUE(lock.owns_lock()); + EXPECT_GE((base::TimeTicks::Now() - start).InMilliseconds(), 20); +} + +TEST(ConditionVariable, WaitForNonPositiveTimeoutReturns) { + base::Mutex mutex; + base::ConditionVariable cv; + Lock lock(mutex); + cv.WaitFor(lock, base::TimeDelta()); + cv.WaitFor(lock, base::Milliseconds(-5)); + EXPECT_TRUE(lock.owns_lock()); +} + +TEST(ConditionVariable, WaitForWakesOnNotify) { + base::Mutex mutex; + base::ConditionVariable cv; + bool ready = false; + Lock lock(mutex); + base::Thread notifier( + "cv-waitfor", + [&] { + base::LockGuard guard(mutex); + ready = true; + cv.NotifyAll(); + }, + /*start_now=*/true); + // A minute-long timeout: returning well before it proves the notify woke us. + const base::TimeTicks start = base::TimeTicks::Now(); + while (!ready) + cv.WaitFor(lock, base::Seconds(60)); + EXPECT_LT((base::TimeTicks::Now() - start).InSeconds(), 30); + lock.unlock(); + ASSERT_TRUE(notifier.Join()); +} + +} // namespace diff --git a/base/threading/condition_variable_win.cc b/base/threading/condition_variable_win.cc new file mode 100644 index 00000000..a8d4bd70 --- /dev/null +++ b/base/threading/condition_variable_win.cc @@ -0,0 +1,37 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include +#include + +// WaitOnAddress and friends live in Synchronization.lib (linked by the build). + +namespace base { + +void ConditionVariable::WaitWhileEquals(u32 seq) { + ::WaitOnAddress(&seq_, &seq, sizeof(seq), INFINITE); +} + +void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { + const i64 us = timeout.InMicroseconds(); + if (us <= 0) + return; + // Round up to whole milliseconds so a short wait does not become a poll, + // and stay below INFINITE; callers re-check and wait again past that. + const i64 ms = (us + 999) / 1000; + const DWORD wait_ms = ms >= static_cast(INFINITE) ? INFINITE - 1 + : static_cast(ms); + ::WaitOnAddress(&seq_, &seq, sizeof(seq), wait_ms); +} + +void ConditionVariable::NotifyOne() { + seq_.fetch_add(1, base::memory_order_relaxed); + ::WakeByAddressSingle(&seq_); +} + +void ConditionVariable::NotifyAll() { + seq_.fetch_add(1, base::memory_order_relaxed); + ::WakeByAddressAll(&seq_); +} + +} // namespace base diff --git a/base/threading/lock_guard.h b/base/threading/lock_guard.h index b9128f16..15126a8f 100644 --- a/base/threading/lock_guard.h +++ b/base/threading/lock_guard.h @@ -78,6 +78,17 @@ class UniqueLock { return *this; } + // Release and reacquire while keeping the association, so a + // base::ConditionVariable can drop the lock across its wait. + void lock() { + m_->lock(); + owns_ = true; + } + void unlock() { + m_->unlock(); + owns_ = false; + } + bool owns_lock() const { return owns_; } explicit operator bool() const { return owns_; } diff --git a/base/threading/thread.h b/base/threading/thread.h index 7ddb7457..33ce5eb8 100644 --- a/base/threading/thread.h +++ b/base/threading/thread.h @@ -75,6 +75,10 @@ inline void SleepForMilliseconds(u64 milliseconds) { // Offer the rest of this thread's time slice to the scheduler. BASE_EXPORT void YieldCurrentThread(); +// Number of online logical processors. Never 0; falls back to 1 when the OS +// cannot say. +BASE_EXPORT u32 GetProcessorCount(); + BASE_EXPORT void SetThreadPriority(Thread::Handle, Thread::Priority new_priority); BASE_EXPORT Thread::Priority GetThreadPriority(Thread::Handle); BASE_EXPORT i32 GetNativeThreadPriority(Thread::Handle); diff --git a/base/threading/thread_posix.cc b/base/threading/thread_posix.cc index bbe63e46..2fe1f0e8 100644 --- a/base/threading/thread_posix.cc +++ b/base/threading/thread_posix.cc @@ -151,6 +151,11 @@ void YieldCurrentThread() { ::sched_yield(); } +u32 GetProcessorCount() { + const long count = ::sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 ? static_cast(count) : 1u; +} + Thread::Handle GetCurrentThreadHandle() { return {.pthread_ = PthreadToHandle(::pthread_self())}; } diff --git a/base/threading/thread_test.cc b/base/threading/thread_test.cc index 1d251094..c6d2adfa 100644 --- a/base/threading/thread_test.cc +++ b/base/threading/thread_test.cc @@ -127,4 +127,8 @@ TEST(Thread, SleepUnblocksAWaitingThread) { EXPECT_EQ(woke.load(), 1); } +TEST(Thread, ProcessorCountIsNonZero) { + EXPECT_GE(base::GetProcessorCount(), 1u); +} + } // namespace diff --git a/base/threading/thread_win.cc b/base/threading/thread_win.cc index f7095e22..9c582dc1 100644 --- a/base/threading/thread_win.cc +++ b/base/threading/thread_win.cc @@ -121,6 +121,13 @@ void YieldCurrentThread() { ::SwitchToThread(); } +u32 GetProcessorCount() { + // ALL_PROCESSOR_GROUPS counts past the 64-processor group a plain + // GetSystemInfo is limited to. + const DWORD count = ::GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); + return count > 0 ? static_cast(count) : 1u; +} + u32 GetCurrentThreadIndex() { return ::GetCurrentThreadId(); }