Implements a thread-safe WeakRef typing.

Bug: dawn:1769
Change-Id: I54f4600a38def5b398e4309cbbc61711ca3f76e3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/138460
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Loko Kung <lokokung@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/dawn/common/BUILD.gn b/src/dawn/common/BUILD.gn
index d1f9520..297565f 100644
--- a/src/dawn/common/BUILD.gn
+++ b/src/dawn/common/BUILD.gn
@@ -275,6 +275,9 @@
       "TypeTraits.h",
       "TypedInteger.h",
       "UnderlyingType.h",
+      "WeakRef.h",
+      "WeakRefSupport.cpp",
+      "WeakRefSupport.h",
       "ityp_array.h",
       "ityp_bitset.h",
       "ityp_span.h",
diff --git a/src/dawn/common/CMakeLists.txt b/src/dawn/common/CMakeLists.txt
index 3b1f0ce..cb1b388 100644
--- a/src/dawn/common/CMakeLists.txt
+++ b/src/dawn/common/CMakeLists.txt
@@ -78,6 +78,9 @@
     "TypeTraits.h"
     "TypedInteger.h"
     "UnderlyingType.h"
+    "WeakRef.h"
+    "WeakRefSupport.cpp"
+    "WeakRefSupport.h"
     "ityp_array.h"
     "ityp_bitset.h"
     "ityp_span.h"
diff --git a/src/dawn/common/Ref.h b/src/dawn/common/Ref.h
index 3daa67e..693a8fac 100644
--- a/src/dawn/common/Ref.h
+++ b/src/dawn/common/Ref.h
@@ -15,11 +15,21 @@
 #ifndef SRC_DAWN_COMMON_REF_H_
 #define SRC_DAWN_COMMON_REF_H_
 
+#include <mutex>
+#include <type_traits>
+
 #include "dawn/common/RefBase.h"
+#include "dawn/common/RefCounted.h"
 
 namespace dawn {
+
+template <typename T>
+class WeakRef;
+
 namespace detail {
 
+class WeakRefSupportBase;
+
 template <typename T>
 struct RefCountedTraits {
     static constexpr T* kNullValue = nullptr;
@@ -33,6 +43,13 @@
 class Ref : public RefBase<T*, detail::RefCountedTraits<T>> {
   public:
     using RefBase<T*, detail::RefCountedTraits<T>>::RefBase;
+
+    template <
+        typename U = T,
+        typename = typename std::enable_if<std::is_base_of_v<detail::WeakRefSupportBase, U>>::type>
+    WeakRef<T> GetWeakRef() {
+        return WeakRef<T>(this->Get());
+    }
 };
 
 }  // namespace dawn
diff --git a/src/dawn/common/RefCounted.h b/src/dawn/common/RefCounted.h
index fd3dcdd..3424271 100644
--- a/src/dawn/common/RefCounted.h
+++ b/src/dawn/common/RefCounted.h
@@ -21,6 +21,10 @@
 
 namespace dawn {
 
+namespace detail {
+class WeakRefData;
+}  // namespace detail
+
 class RefCount {
   public:
     // Create a refcount with a payload. The refcount starts initially at one.
@@ -87,6 +91,9 @@
     void APIRelease() { ReleaseAndLockBeforeDestroy(); }
 
   protected:
+    // Friend class is needed to access the RefCount to TryIncrement.
+    friend class detail::WeakRefData;
+
     virtual ~RefCounted();
 
     void ReleaseAndLockBeforeDestroy();
diff --git a/src/dawn/common/WeakRef.h b/src/dawn/common/WeakRef.h
new file mode 100644
index 0000000..4764924
--- /dev/null
+++ b/src/dawn/common/WeakRef.h
@@ -0,0 +1,77 @@
+// Copyright 2023 The Dawn Authors
+//
+// 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
+//
+//     http://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 SRC_DAWN_COMMON_WEAKREF_H_
+#define SRC_DAWN_COMMON_WEAKREF_H_
+
+#include <utility>
+
+#include "dawn/common/Ref.h"
+#include "dawn/common/WeakRefSupport.h"
+
+namespace dawn {
+
+template <typename T>
+class WeakRef {
+  public:
+    WeakRef() {}
+
+    // Constructors from nullptr.
+    // NOLINTNEXTLINE(runtime/explicit)
+    constexpr WeakRef(std::nullptr_t) : WeakRef() {}
+
+    // Constructors from a WeakRef<U>, where U can also equal T.
+    template <typename U, typename = typename std::enable_if<std::is_base_of_v<T, U>>::type>
+    WeakRef(const WeakRef<U>& other) : mData(other.mData) {}
+    template <typename U, typename = typename std::enable_if<std::is_base_of_v<T, U>>::type>
+    WeakRef<T>& operator=(const WeakRef<U>& other) {
+        mData = other.mData;
+        return *this;
+    }
+    template <typename U, typename = typename std::enable_if<std::is_base_of_v<T, U>>::type>
+    WeakRef(WeakRef<U>&& other) : mData(std::move(other.mData)) {}
+    template <typename U, typename = typename std::enable_if<std::is_base_of_v<T, U>>::type>
+    WeakRef<T>& operator=(WeakRef<U>&& other) {
+        if (&other != this) {
+            mData = std::move(other.mData);
+        }
+        return *this;
+    }
+
+    // Promotes a WeakRef to a Ref. Access to the raw pointer is not allowed because a raw pointer
+    // could become invalid after being retrieved.
+    Ref<T> Promote() {
+        if (mData != nullptr) {
+            return AcquireRef(static_cast<T*>(mData->TryGetRef().Detach()));
+        }
+        return nullptr;
+    }
+
+  private:
+    // Friend is needed so that we can access the data ref in conversions.
+    template <typename U>
+    friend class WeakRef;
+    // Friend is needed to access the private constructor.
+    template <typename U>
+    friend class Ref;
+
+    // Constructor from data should only be allowed via the GetWeakRef function.
+    explicit WeakRef(detail::WeakRefSupportBase* data) : mData(data->mData) {}
+
+    Ref<detail::WeakRefData> mData = nullptr;
+};
+
+}  // namespace dawn
+
+#endif  // SRC_DAWN_COMMON_WEAKREFCOUNTED_H_
diff --git a/src/dawn/common/WeakRefSupport.cpp b/src/dawn/common/WeakRefSupport.cpp
new file mode 100644
index 0000000..9273ff1
--- /dev/null
+++ b/src/dawn/common/WeakRefSupport.cpp
@@ -0,0 +1,42 @@
+// Copyright 2023 The Dawn Authors
+//
+// 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
+//
+//     http://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 "dawn/common/WeakRefSupport.h"
+
+#include <utility>
+
+namespace dawn::detail {
+
+WeakRefData::WeakRefData(RefCounted* value) : mValue(value) {}
+
+Ref<RefCounted> WeakRefData::TryGetRef() {
+    std::lock_guard<std::mutex> lock(mMutex);
+    if (!mValue || !mValue->mRefCount.TryIncrement()) {
+        return nullptr;
+    }
+    return AcquireRef(mValue);
+}
+
+void WeakRefData::Invalidate() {
+    std::lock_guard<std::mutex> lock(mMutex);
+    mValue = nullptr;
+}
+
+WeakRefSupportBase::WeakRefSupportBase(Ref<detail::WeakRefData> data) : mData(std::move(data)) {}
+
+WeakRefSupportBase::~WeakRefSupportBase() {
+    mData->Invalidate();
+}
+
+}  // namespace dawn::detail
diff --git a/src/dawn/common/WeakRefSupport.h b/src/dawn/common/WeakRefSupport.h
new file mode 100644
index 0000000..9702905
--- /dev/null
+++ b/src/dawn/common/WeakRefSupport.h
@@ -0,0 +1,67 @@
+// Copyright 2023 The Dawn Authors
+//
+// 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
+//
+//     http://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 SRC_DAWN_COMMON_WEAKREFSUPPORT_H_
+#define SRC_DAWN_COMMON_WEAKREFSUPPORT_H_
+
+#include "dawn/common/Ref.h"
+
+namespace dawn {
+
+template <typename T>
+class WeakRef;
+
+namespace detail {
+
+// Indirection layer to provide external ref-counting for WeakRefs.
+class WeakRefData : public RefCounted {
+  public:
+    explicit WeakRefData(RefCounted* value);
+    void Invalidate();
+
+    // Tries to return a valid Ref to `mValue` if it's internal refcount is not already 0. If the
+    // internal refcount has already reached 0, returns nullptr instead.
+    Ref<RefCounted> TryGetRef();
+
+  private:
+    std::mutex mMutex;
+    RefCounted* mValue = nullptr;
+};
+
+// Interface base class used to enable compile-time verification of WeakRefSupport functions.
+class WeakRefSupportBase {
+  protected:
+    explicit WeakRefSupportBase(Ref<detail::WeakRefData> data);
+    ~WeakRefSupportBase();
+
+  private:
+    template <typename T>
+    friend class ::dawn::WeakRef;
+
+    Ref<detail::WeakRefData> mData;
+};
+
+}  // namespace detail
+
+// Class that should be extended to enable WeakRefs for the type.
+template <typename T>
+class WeakRefSupport : public detail::WeakRefSupportBase {
+  public:
+    WeakRefSupport()
+        : WeakRefSupportBase(AcquireRef(new detail::WeakRefData(static_cast<T*>(this)))) {}
+};
+
+}  // namespace dawn
+
+#endif  // SRC_DAWN_COMMON_WEAKREFSUPPORT_H_
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 4c58b9d..4ef3258 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -325,6 +325,7 @@
     "unittests/ToggleTests.cpp",
     "unittests/TypedIntegerTests.cpp",
     "unittests/UnicodeTests.cpp",
+    "unittests/WeakRefTests.cpp",
     "unittests/native/AllowedErrorTests.cpp",
     "unittests/native/BlobTests.cpp",
     "unittests/native/CacheRequestTests.cpp",
diff --git a/src/dawn/tests/unittests/ContentLessObjectCacheTests.cpp b/src/dawn/tests/unittests/ContentLessObjectCacheTests.cpp
index 0b86e15..4ad91c6 100644
--- a/src/dawn/tests/unittests/ContentLessObjectCacheTests.cpp
+++ b/src/dawn/tests/unittests/ContentLessObjectCacheTests.cpp
@@ -20,11 +20,14 @@
 #include <vector>
 
 #include "dawn/common/ContentLessObjectCache.h"
+#include "dawn/utils/BinarySemaphore.h"
 #include "gtest/gtest.h"
 
 namespace dawn {
 namespace {
 
+using utils::BinarySemaphore;
+
 class RefCountedT : public RefCounted {
   public:
     explicit RefCountedT(size_t value) : mValue(value) {}
@@ -114,26 +117,6 @@
     EXPECT_FALSE(cache.Empty());
 }
 
-// Helper struct that basically acts as a semaphore to allow for flow control in multiple threads.
-struct Signal {
-    std::mutex mutex;
-    std::condition_variable cv;
-    bool signaled = false;
-
-    void Fire() {
-        std::lock_guard<std::mutex> lock(mutex);
-        signaled = true;
-        cv.notify_one();
-    }
-    void Wait() {
-        std::unique_lock<std::mutex> lock(mutex);
-        while (!signaled) {
-            cv.wait(lock);
-        }
-        signaled = false;
-    }
-};
-
 // Inserting and finding elements should respect the results from the insert call.
 TEST(ContentLessObjectCacheTest, InsertingAndFinding) {
     constexpr size_t kNumObjects = 100;
@@ -169,12 +152,12 @@
 
 // Finding an element that is in the process of deletion should return nullptr.
 TEST(ContentLessObjectCacheTest, FindDeleting) {
-    Signal signalA, signalB;
+    BinarySemaphore semA, semB;
 
     ContentLessObjectCache<RefCountedT> cache;
     Ref<RefCountedT> object = AcquireRef(new RefCountedT(1, [&](RefCountedT* x) {
-        signalA.Fire();
-        signalB.Wait();
+        semA.Release();
+        semB.Acquire();
         cache.Erase(x);
     }));
     EXPECT_TRUE(cache.Insert(object.Get()).second);
@@ -183,10 +166,10 @@
     auto threadA = [&] { object = nullptr; };
     // Thread B will try to Find the entry before it is completely destroyed.
     auto threadB = [&] {
-        signalA.Wait();
+        semA.Acquire();
         RefCountedT blueprint(1);
         EXPECT_TRUE(cache.Find(&blueprint) == nullptr);
-        signalB.Fire();
+        semB.Release();
     };
 
     std::thread tA(threadA);
@@ -198,12 +181,12 @@
 // Inserting an element that has an entry which is in process of deletion should insert the new
 // object.
 TEST(ContentLessObjectCacheTest, InsertDeleting) {
-    Signal signalA, signalB;
+    BinarySemaphore semA, semB;
 
     ContentLessObjectCache<RefCountedT> cache;
     Ref<RefCountedT> object1 = AcquireRef(new RefCountedT(1, [&](RefCountedT* x) {
-        signalA.Fire();
-        signalB.Wait();
+        semA.Release();
+        semB.Acquire();
         cache.Erase(x);
     }));
     EXPECT_TRUE(cache.Insert(object1.Get()).second);
@@ -216,9 +199,9 @@
     // Thread B will try to Insert a hash equivalent entry before the original is completely
     // destroyed.
     auto threadB = [&] {
-        signalA.Wait();
+        semA.Acquire();
         EXPECT_TRUE(cache.Insert(object2.Get()).second);
-        signalB.Fire();
+        semB.Release();
     };
 
     std::thread tA(threadA);
diff --git a/src/dawn/tests/unittests/WeakRefTests.cpp b/src/dawn/tests/unittests/WeakRefTests.cpp
new file mode 100644
index 0000000..5ae62f3
--- /dev/null
+++ b/src/dawn/tests/unittests/WeakRefTests.cpp
@@ -0,0 +1,170 @@
+// Copyright 2023 The Dawn Authors
+//
+// 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
+//
+//     http://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 <functional>
+#include <thread>
+
+#include "dawn/common/WeakRef.h"
+#include "dawn/common/WeakRefSupport.h"
+#include "dawn/utils/BinarySemaphore.h"
+#include "gtest/gtest.h"
+
+namespace dawn {
+namespace {
+
+using utils::BinarySemaphore;
+
+class RefCountedT : public RefCounted {};
+
+class WeakRefBaseA : public RefCounted, public WeakRefSupport<WeakRefBaseA> {
+  public:
+    WeakRefBaseA() = default;
+    explicit WeakRefBaseA(std::function<void(WeakRefBaseA*)> deleteFn) : mDeleteFn(deleteFn) {}
+
+  protected:
+    ~WeakRefBaseA() override { mDeleteFn(this); }
+
+  private:
+    std::function<void(WeakRefBaseA*)> mDeleteFn = [](WeakRefBaseA*) -> void {};
+};
+
+class WeakRefDerivedA : public WeakRefBaseA {
+  public:
+    WeakRefDerivedA() = default;
+    explicit WeakRefDerivedA(std::function<void(WeakRefBaseA*)> deleteFn)
+        : WeakRefBaseA(deleteFn) {}
+};
+
+class WeakRefBaseB : public RefCounted, public WeakRefSupport<WeakRefBaseB> {};
+class WeakRefDerivedB : public WeakRefBaseB {};
+
+// When the original refcounted object is destroyed, all WeakRefs are no longer able to Promote.
+TEST(WeakRefTests, BasicPromote) {
+    Ref<WeakRefBaseA> base = AcquireRef(new WeakRefBaseA());
+    WeakRef<WeakRefBaseA> weak = base.GetWeakRef();
+    EXPECT_EQ(weak.Promote().Get(), base.Get());
+
+    base = nullptr;
+    EXPECT_EQ(weak.Promote().Get(), nullptr);
+}
+
+// When the original refcounted object is destroyed, all WeakRefs, including upcasted ones, are no
+// longer able to Promote.
+TEST(WeakRefTests, DerivedPromote) {
+    Ref<WeakRefDerivedA> base = AcquireRef(new WeakRefDerivedA());
+    WeakRef<WeakRefDerivedA> weak1 = base.GetWeakRef();
+    WeakRef<WeakRefBaseA> weak2 = weak1;
+    WeakRef<WeakRefBaseA> weak3 = base.GetWeakRef();
+    EXPECT_EQ(weak1.Promote().Get(), base.Get());
+    EXPECT_EQ(weak2.Promote().Get(), base.Get());
+    EXPECT_EQ(weak3.Promote().Get(), base.Get());
+
+    base = nullptr;
+    EXPECT_EQ(weak1.Promote().Get(), nullptr);
+    EXPECT_EQ(weak2.Promote().Get(), nullptr);
+    EXPECT_EQ(weak3.Promote().Get(), nullptr);
+}
+
+// Trying to promote a WeakRef to a Ref while the original value is being destroyed returns nullptr.
+TEST(WeakRefTests, DeletingAndPromoting) {
+    BinarySemaphore semA, semB;
+    Ref<WeakRefBaseA> base = AcquireRef(new WeakRefBaseA([&](WeakRefBaseA*) {
+        semB.Release();
+        semA.Acquire();
+    }));
+
+    auto f = [&] {
+        WeakRef<WeakRefBaseA> weak = base.GetWeakRef();
+        semA.Release();
+        semB.Acquire();
+        EXPECT_EQ(weak.Promote().Get(), nullptr);
+        semA.Release();
+    };
+    std::thread t(f);
+
+    semA.Acquire();
+    base = nullptr;
+    t.join();
+}
+
+}  // anonymous namespace
+}  // namespace dawn
+
+// Special compilation tests that are only enabled when experimental headers are available.
+#if __has_include(<experimental/type_traits>)
+#include <experimental/type_traits>
+
+namespace dawn {
+namespace {
+
+// Helper detection utilities for verifying that unintended assignments are not allowed.
+template <typename L, typename R>
+using weakref_copyable_t =
+    decltype(std::declval<WeakRef<L>&>() = std::declval<const WeakRef<R>&>());
+template <typename L, typename R>
+using weakref_movable_t =
+    decltype(std::declval<WeakRef<L>&>() = std::declval<const WeakRef<R>&&>());
+TEST(WeakRefTests, CrossTypesAssignments) {
+    // Same type and upcasting is allowed.
+    static_assert(std::experimental::is_detected_v<weakref_copyable_t, WeakRefBaseA, WeakRefBaseA>,
+                  "Same type copy assignment is allowed.");
+    static_assert(std::experimental::is_detected_v<weakref_movable_t, WeakRefBaseA, WeakRefBaseA>,
+                  "Same type move assignment is allowed.");
+
+    static_assert(
+        std::experimental::is_detected_v<weakref_copyable_t, WeakRefBaseA, WeakRefDerivedA>,
+        "Upcasting type copy assignment is allowed.");
+    static_assert(
+        std::experimental::is_detected_v<weakref_movable_t, WeakRefBaseA, WeakRefDerivedA>,
+        "Upcasting type move assignment is allowed.");
+
+    // Same type, but down casting is not allowed.
+    static_assert(
+        !std::experimental::is_detected_v<weakref_copyable_t, WeakRefDerivedA, WeakRefBaseA>,
+        "Downcasting type copy assignment is not allowed.");
+    static_assert(
+        !std::experimental::is_detected_v<weakref_movable_t, WeakRefDerivedA, WeakRefBaseA>,
+        "Downcasting type move assignment is not allowed.");
+
+    // Cross types are not allowed.
+    static_assert(!std::experimental::is_detected_v<weakref_copyable_t, WeakRefBaseA, WeakRefBaseB>,
+                  "Cross type copy assignment is not allowed.");
+    static_assert(!std::experimental::is_detected_v<weakref_movable_t, WeakRefBaseA, WeakRefBaseB>,
+                  "Cross type move assignment is not allowed.");
+    static_assert(
+        !std::experimental::is_detected_v<weakref_copyable_t, WeakRefBaseA, WeakRefDerivedB>,
+        "Cross type upcasting copy assignment is not allowed.");
+    static_assert(
+        !std::experimental::is_detected_v<weakref_movable_t, WeakRefBaseA, WeakRefDerivedB>,
+        "Cross type upcasting move assignment is not allowed.");
+}
+
+// Helper detection utilty to verify whether GetWeakRef is enabled.
+template <typename T>
+using can_get_weakref_t = decltype(std::declval<Ref<T>>().GetWeakRef());
+TEST(WeakRefTests, GetWeakRef) {
+    // The GetWeakRef function is only available on types that extend WeakRefSupport.
+    static_assert(std::experimental::is_detected_v<can_get_weakref_t, WeakRefBaseA>,
+                  "GetWeakRef is enabled on classes that directly extend WeakRefSupport.");
+    static_assert(std::experimental::is_detected_v<can_get_weakref_t, WeakRefDerivedA>,
+                  "GetWeakRef is enabled on classes that indirectly extend WeakRefSupport.");
+
+    static_assert(!std::experimental::is_detected_v<can_get_weakref_t, RefCountedT>,
+                  "GetWeakRef is disabled on classes that do not extend WeakRefSupport.");
+}
+
+}  // anonymous namespace
+}  // namespace dawn
+
+#endif
diff --git a/src/dawn/utils/BUILD.gn b/src/dawn/utils/BUILD.gn
index 48430d4..f83b679 100644
--- a/src/dawn/utils/BUILD.gn
+++ b/src/dawn/utils/BUILD.gn
@@ -27,6 +27,8 @@
   ]
 
   sources = [
+    "BinarySemaphore.cpp",
+    "BinarySemaphore.h",
     "ComboRenderBundleEncoderDescriptor.cpp",
     "ComboRenderBundleEncoderDescriptor.h",
     "ComboRenderPipelineDescriptor.cpp",
diff --git a/src/dawn/utils/BinarySemaphore.cpp b/src/dawn/utils/BinarySemaphore.cpp
new file mode 100644
index 0000000..a9bedf7
--- /dev/null
+++ b/src/dawn/utils/BinarySemaphore.cpp
@@ -0,0 +1,33 @@
+// Copyright 2023 The Dawn Authors
+//
+// 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
+//
+//     http://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 "dawn/utils/BinarySemaphore.h"
+
+namespace dawn::utils {
+
+void BinarySemaphore::Release() {
+    std::lock_guard<std::mutex> lock(mMutex);
+    mSignaled = true;
+    mCv.notify_one();
+}
+
+void BinarySemaphore::Acquire() {
+    std::unique_lock<std::mutex> lock(mMutex);
+    while (!mSignaled) {
+        mCv.wait(lock);
+    }
+    mSignaled = false;
+}
+
+}  // namespace dawn::utils
diff --git a/src/dawn/utils/BinarySemaphore.h b/src/dawn/utils/BinarySemaphore.h
new file mode 100644
index 0000000..278ac92
--- /dev/null
+++ b/src/dawn/utils/BinarySemaphore.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The Dawn Authors
+//
+// 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
+//
+//     http://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 SRC_DAWN_UTILS_BINARYSEMAPHORE_H_
+#define SRC_DAWN_UTILS_BINARYSEMAPHORE_H_
+
+#include <condition_variable>
+#include <mutex>
+
+namespace dawn::utils {
+
+// Binary semaphore implementation. Could eventually be replaced with std::binary_semaphore when
+// C++20 is available everywhere.
+class BinarySemaphore {
+  public:
+    void Release();
+    void Acquire();
+
+  private:
+    std::mutex mMutex;
+    std::condition_variable mCv;
+    bool mSignaled = false;
+};
+
+}  // namespace dawn::utils
+
+#endif  // SRC_DAWN_UTILS_BINARYSEMAPHORE_H_
diff --git a/src/dawn/utils/CMakeLists.txt b/src/dawn/utils/CMakeLists.txt
index b2595de..d5c2059 100644
--- a/src/dawn/utils/CMakeLists.txt
+++ b/src/dawn/utils/CMakeLists.txt
@@ -15,6 +15,8 @@
 add_library(dawn_utils STATIC ${DAWN_PLACEHOLDER_FILE})
 common_compile_options(dawn_utils)
 target_sources(dawn_utils PRIVATE
+    "BinarySemaphore.cpp"
+    "BinarySemaphore.h"
     "ComboRenderBundleEncoderDescriptor.cpp"
     "ComboRenderBundleEncoderDescriptor.h"
     "ComboRenderPipelineDescriptor.cpp"