[dawn] Make spontaneous device callbacks thread safe in native also.

- Unifies the code used for a lot of the device callbacks between the
  wire client and native. Note that I am currently using dawn/common
  for convenience, but it might be better in the long run to use
  another target or something since I needed to explicitly exclude
  the helpers for WASM builds since the logging callback is currently
  native only.

Change-Id: Ia244a3dab0c9244d6a277c31d857210c9d3fc554
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/297575
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Loko Kung <lokokung@google.com>
diff --git a/src/dawn/common/BUILD.gn b/src/dawn/common/BUILD.gn
index 6b5264a..d6b5c5e 100644
--- a/src/dawn/common/BUILD.gn
+++ b/src/dawn/common/BUILD.gn
@@ -401,6 +401,13 @@
     ]
     sources += get_target_outputs(":dawn_gpu_info_gen")
 
+    if (!is_wasm) {
+      sources += [
+        "WGPUDeviceCallbackInfos.cpp",
+        "WGPUDeviceCallbackInfos.h",
+      ]
+    }
+
     public_deps = [
       ":dawn_gpu_info_gen",
       ":dawn_version_gen",
diff --git a/src/dawn/common/CMakeLists.txt b/src/dawn/common/CMakeLists.txt
index bc70590..953f412 100644
--- a/src/dawn/common/CMakeLists.txt
+++ b/src/dawn/common/CMakeLists.txt
@@ -133,7 +133,7 @@
 set(conditional_private_depends)
 
 if (WIN32)
-    list(APPEND headers
+    list(APPEND private_headers
         "windows_with_undefs.h"
         "WindowsUtils.h"
     )
@@ -141,7 +141,7 @@
         "WindowsUtils.cpp"
     )
 elseif(APPLE)
-    list(APPEND headers
+    list(APPEND private_headers
         "IOSurfaceUtils.h"
     )
     list(APPEND sources
@@ -154,6 +154,15 @@
     )
 endif()
 
+if (NOT EMSCRIPTEN)
+    list(APPEND private_headers
+        "WGPUDeviceCallbackInfos.h"
+    )
+    list(APPEND sources
+        "WGPUDeviceCallbackInfos.cpp"
+    )
+endif()
+
 if (CMAKE_SYSTEM_NAME STREQUAL "Android")
     find_library(log_lib log)
     list(APPEND conditional_private_depends
diff --git a/src/dawn/common/WGPUDeviceCallbackInfos.cpp b/src/dawn/common/WGPUDeviceCallbackInfos.cpp
new file mode 100644
index 0000000..63b6d95
--- /dev/null
+++ b/src/dawn/common/WGPUDeviceCallbackInfos.cpp
@@ -0,0 +1,181 @@
+// Copyright 2026 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "dawn/common/WGPUDeviceCallbackInfos.h"
+
+#include "dawn/common/Log.h"
+
+namespace dawn {
+namespace {
+// Default callback infos depending on the build type.
+#ifdef DAWN_ENABLE_ASSERTS
+static constexpr WGPUDeviceLostCallbackInfo kDefaultDeviceLostCallbackInfo = {
+    nullptr, WGPUCallbackMode_AllowSpontaneous,
+    [](WGPUDevice const*, WGPUDeviceLostReason, WGPUStringView, void*, void*) {
+        static std::once_flag flag;
+        std::call_once(flag, []() {
+            dawn::WarningLog() << "No Dawn device lost callback was set. This is probably not "
+                                  "intended. If you really want to ignore device lost "
+                                  "and suppress this message, set the callback explicitly.";
+        });
+    },
+    nullptr, nullptr};
+static constexpr WGPUUncapturedErrorCallbackInfo kDefaultUncapturedErrorCallbackInfo = {
+    nullptr,
+    [](WGPUDevice const*, WGPUErrorType, WGPUStringView, void*, void*) {
+        static std::once_flag flag;
+        std::call_once(flag, []() {
+            dawn::WarningLog() << "No Dawn device uncaptured error callback was set. This is "
+                                  "probably not intended. If you really want to ignore errors "
+                                  "and suppress this message, set the callback explicitly.";
+        });
+    },
+    nullptr, nullptr};
+static constexpr WGPULoggingCallbackInfo kDefaultLoggingCallbackInfo = {
+    nullptr,
+    [](WGPULoggingType, WGPUStringView, void*, void*) {
+        static std::once_flag flag;
+        std::call_once(flag, []() {
+            dawn::WarningLog() << "No Dawn device logging callback callback was set. This is "
+                                  "probably not intended. If you really want to ignore logs "
+                                  "and suppress this message, set the callback explicitly.";
+        });
+    },
+    nullptr, nullptr};
+#else
+static constexpr WGPUDeviceLostCallbackInfo kDefaultDeviceLostCallbackInfo = {
+    nullptr, WGPUCallbackMode_AllowSpontaneous, nullptr, nullptr, nullptr};
+static constexpr WGPUUncapturedErrorCallbackInfo kDefaultUncapturedErrorCallbackInfo = {
+    nullptr, nullptr, nullptr, nullptr};
+static constexpr WGPULoggingCallbackInfo kDefaultLoggingCallbackInfo = {nullptr, nullptr, nullptr,
+                                                                        nullptr};
+#endif  // DAWN_ENABLE_ASSERTS
+
+const WGPUUncapturedErrorCallbackInfo& GetUncapturedErrorCallbackInfoOrDefault(
+    const WGPUDeviceDescriptor* descriptor) {
+    if (descriptor != nullptr && descriptor->uncapturedErrorCallbackInfo.callback != nullptr) {
+        return descriptor->uncapturedErrorCallbackInfo;
+    }
+    return kDefaultUncapturedErrorCallbackInfo;
+}
+}  // namespace
+
+const WGPUDeviceLostCallbackInfo& GetDeviceLostCallbackInfoOrDefault(
+    const WGPUDeviceDescriptor* descriptor) {
+    if (descriptor != nullptr && descriptor->deviceLostCallbackInfo.callback != nullptr) {
+        return descriptor->deviceLostCallbackInfo;
+    }
+    return kDefaultDeviceLostCallbackInfo;
+}
+
+WGPUDeviceCallbackInfos::CallbackInfos::CallbackInfos() = default;
+
+WGPUDeviceCallbackInfos::CallbackInfos::CallbackInfos(const WGPUUncapturedErrorCallbackInfo& error,
+                                                      const WGPULoggingCallbackInfo& logging) {
+    if (error.callback != nullptr) {
+        this->error = error;
+    }
+    if (logging.callback != nullptr) {
+        this->logging = logging;
+    }
+}
+
+WGPUDeviceCallbackInfos::WGPUDeviceCallbackInfos() = default;
+
+WGPUDeviceCallbackInfos::WGPUDeviceCallbackInfos(const WGPUDeviceDescriptor* descriptor)
+    : mCallbackInfos(GetUncapturedErrorCallbackInfoOrDefault(descriptor),
+                     kDefaultLoggingCallbackInfo) {}
+
+void WGPUDeviceCallbackInfos::CallErrorCallback(WGPUDevice const* device,
+                                                WGPUErrorType type,
+                                                WGPUStringView message) {
+    std::optional<WGPUUncapturedErrorCallbackInfo> callbackInfo;
+    mCallbackInfos.Use<NotifyType::None>([&](auto callbackInfos) {
+        callbackInfo = callbackInfos->error;
+        if (callbackInfo) {
+            callbackInfos->semaphore += 1;
+        }
+    });
+
+    // If we don't have a callback info, we can just return.
+    if (!callbackInfo) {
+        return;
+    }
+
+    // Call the callback without holding the lock to prevent any re-entrant issues.
+    DAWN_ASSERT(callbackInfo->callback != nullptr);
+    callbackInfo->callback(device, type, message, callbackInfo->userdata1, callbackInfo->userdata2);
+
+    mCallbackInfos.Use([&](auto callbackInfos) {
+        DAWN_ASSERT(callbackInfos->semaphore > 0);
+        callbackInfos->semaphore -= 1;
+    });
+}
+
+void WGPUDeviceCallbackInfos::CallLoggingCallback(WGPULoggingType type, WGPUStringView message) {
+    std::optional<WGPULoggingCallbackInfo> callbackInfo;
+    mCallbackInfos.Use<NotifyType::None>([&](auto callbackInfos) {
+        callbackInfo = callbackInfos->logging;
+        if (callbackInfo) {
+            callbackInfos->semaphore += 1;
+        }
+    });
+
+    // If we don't have a callback info, we can just return.
+    if (!callbackInfo) {
+        return;
+    }
+
+    // Call the callback without holding the lock to prevent any re-entrant issues.
+    DAWN_ASSERT(callbackInfo->callback != nullptr);
+    callbackInfo->callback(type, message, callbackInfo->userdata1, callbackInfo->userdata2);
+
+    mCallbackInfos.Use([&](auto callbackInfos) {
+        DAWN_ASSERT(callbackInfos->semaphore > 0);
+        callbackInfos->semaphore -= 1;
+    });
+}
+
+void WGPUDeviceCallbackInfos::SetLoggingCallbackInfo(const WGPULoggingCallbackInfo& callbackInfo) {
+    mCallbackInfos.Use<NotifyType::None>(
+        [&](auto callbackInfos) { callbackInfos->logging = callbackInfo; });
+}
+
+void WGPUDeviceCallbackInfos::Clear() {
+    mCallbackInfos.Use<NotifyType::None>([](auto callbackInfos) {
+        callbackInfos->error = std::nullopt;
+        callbackInfos->logging = std::nullopt;
+
+        // The uncaptured error and logging callbacks are spontaneous and must not be called
+        // after we call the device lost's |mCallback| below. Although we have cleared those
+        // callbacks, we need to wait for any remaining outstanding callbacks to finish before
+        // continuing.
+        callbackInfos.Wait([](auto& x) { return x.semaphore == 0; });
+    });
+}
+
+}  // namespace dawn
diff --git a/src/dawn/common/WGPUDeviceCallbackInfos.h b/src/dawn/common/WGPUDeviceCallbackInfos.h
new file mode 100644
index 0000000..e804f7b
--- /dev/null
+++ b/src/dawn/common/WGPUDeviceCallbackInfos.h
@@ -0,0 +1,85 @@
+// Copyright 2026 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_DAWN_COMMON_WGPUDEVICECALLBACKINFOS_H_
+#define SRC_DAWN_COMMON_WGPUDEVICECALLBACKINFOS_H_
+
+#include <webgpu/webgpu.h>
+
+#include <optional>
+
+#include "dawn/common/MutexProtected.h"
+
+namespace dawn {
+
+const WGPUDeviceLostCallbackInfo& GetDeviceLostCallbackInfoOrDefault(
+    const WGPUDeviceDescriptor* descriptor);
+
+// Device level unconditionally spontaneous callbacks need to be synchronized so this class provides
+// a common wrapper for those callback infos so that the implementation can be shared across native
+// and wire client.
+class WGPUDeviceCallbackInfos {
+  public:
+    WGPUDeviceCallbackInfos();
+    explicit WGPUDeviceCallbackInfos(const WGPUDeviceDescriptor* descriptor);
+
+    // APIs to call the callbacks.
+    void CallErrorCallback(WGPUDevice const* device, WGPUErrorType type, WGPUStringView message);
+    void CallLoggingCallback(WGPULoggingType type, WGPUStringView message);
+
+    // The logging callback currently needs to support a setter.
+    void SetLoggingCallbackInfo(const WGPULoggingCallbackInfo& callbackInfo);
+
+    // Used when the device is lost and we want to clear out the callbacks. This helper waits until
+    // there are no other places using the callbacks before returning. This is important since this
+    // is generally used when completing the device lost event which users may use to clean up the
+    // uncaptured error and logging callbacks.
+    void Clear();
+
+  private:
+    struct CallbackInfos {
+        CallbackInfos();
+        CallbackInfos(const WGPUUncapturedErrorCallbackInfo& error,
+                      const WGPULoggingCallbackInfo& logging);
+
+        // The callback infos are optional because once the device is lost, they are set to
+        // std::nullopt and no longer do anything.
+        std::optional<WGPUUncapturedErrorCallbackInfo> error = std::nullopt;
+        std::optional<WGPULoggingCallbackInfo> logging = std::nullopt;
+
+        // Counter that tracks how many places are currently using callback infos. This is used to
+        // ensure that before we call the device lost callback (which may deallocate the uncaptured
+        // error and logging callbacks), we have ensured that there are no outstanding references to
+        // those callbacks.
+        uint32_t semaphore = 0;
+    };
+    MutexCondVarProtected<CallbackInfos> mCallbackInfos;
+};
+
+}  // namespace dawn
+
+#endif  // SRC_DAWN_COMMON_WGPUDEVICECALLBACKINFOS_H_
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 6430b9d..4fc1c46 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -143,11 +143,6 @@
 
 namespace {
 
-static constexpr WGPUUncapturedErrorCallbackInfo kEmptyUncapturedErrorCallbackInfo = {
-    nullptr, nullptr, nullptr, nullptr};
-static constexpr WGPULoggingCallbackInfo kEmptyLoggingCallbackInfo = {nullptr, nullptr, nullptr,
-                                                                      nullptr};
-
 void TrimErrorScopeStacks(
     absl::flat_hash_map<ThreadUniqueId, std::unique_ptr<ErrorScopeStack>>& errorScopeStacks) {
     for (auto it = errorScopeStacks.begin(); it != errorScopeStacks.end();) {
@@ -176,30 +171,8 @@
 Ref<DeviceBase::DeviceLostEvent> DeviceBase::DeviceLostEvent::Create(
     const DeviceDescriptor* descriptor) {
     DAWN_ASSERT(descriptor != nullptr);
-
-#if defined(DAWN_ENABLE_ASSERTS)
-    static constexpr WGPUDeviceLostCallbackInfo kDefaultDeviceLostCallbackInfo = {
-        nullptr, WGPUCallbackMode_AllowSpontaneous,
-        [](WGPUDevice const*, WGPUDeviceLostReason, WGPUStringView, void*, void*) {
-            static bool calledOnce = false;
-            if (!calledOnce) {
-                calledOnce = true;
-                dawn::WarningLog() << "No Dawn device lost callback was set. This is probably not "
-                                      "intended. If you really want to ignore device lost and "
-                                      "suppress this message, set the callback explicitly.";
-            }
-        },
-        nullptr, nullptr};
-#else
-    static constexpr WGPUDeviceLostCallbackInfo kDefaultDeviceLostCallbackInfo = {
-        nullptr, WGPUCallbackMode_AllowProcessEvents, nullptr, nullptr, nullptr};
-#endif  // DAWN_ENABLE_ASSERTS
-
-    WGPUDeviceLostCallbackInfo deviceLostCallbackInfo = kDefaultDeviceLostCallbackInfo;
-    if (descriptor->deviceLostCallbackInfo.callback != nullptr) {
-        deviceLostCallbackInfo = descriptor->deviceLostCallbackInfo;
-    }
-    return AcquireRef(new DeviceBase::DeviceLostEvent(deviceLostCallbackInfo));
+    return AcquireRef(
+        new DeviceBase::DeviceLostEvent(GetDeviceLostCallbackInfoOrDefault(ToAPI(descriptor))));
 }
 
 void DeviceBase::DeviceLostEvent::SetLost(EventManager* eventManager,
@@ -216,15 +189,11 @@
         mMessage = "A valid external Instance reference no longer exists.";
     }
 
-    // Some users may use the device lost callback to deallocate resources allocated for the
-    // uncaptured error and logging callbacks, so reset these callbacks before calling the
-    // device lost callback.
     if (mDevice != nullptr) {
-        mDevice->mUncapturedErrorCallbackInfo = kEmptyUncapturedErrorCallbackInfo;
-        {
-            std::lock_guard<std::shared_mutex> lock(mDevice->mLoggingMutex);
-            mDevice->mLoggingCallbackInfo = kEmptyLoggingCallbackInfo;
-        }
+        // The uncaptured error and logging callbacks are spontaneous and must not be called
+        // after we call the device lost's |mCallback| below, so we clear them and wait for them to
+        // be no longer referenced before moving forwards.
+        mDevice->mCallbackInfos.Clear();
     }
 
     auto device = ToAPI(mDevice.Get());
@@ -301,6 +270,7 @@
                        const TogglesState& deviceToggles,
                        Ref<DeviceLostEvent>&& lostEvent)
     : mLostEvent(std::move(lostEvent)),
+      mCallbackInfos(ToAPI(*descriptor)),
       mAdapter(adapter),
       mToggles(deviceToggles),
       mNextPipelineCompatibilityToken(1) {
@@ -309,45 +279,6 @@
     DAWN_ASSERT(mLostEvent);
     mLostEvent->mDevice = this;
 
-#if defined(DAWN_ENABLE_ASSERTS)
-    static constexpr WGPUUncapturedErrorCallbackInfo kDefaultUncapturedErrorCallbackInfo = {
-        nullptr,
-        [](WGPUDevice const*, WGPUErrorType, WGPUStringView, void*, void*) {
-            static bool calledOnce = false;
-            if (!calledOnce) {
-                calledOnce = true;
-                dawn::WarningLog() << "No Dawn device uncaptured error callback was set. This is "
-                                      "probably not intended. If you really want to ignore errors "
-                                      "and suppress this message, set the callback explicitly.";
-            }
-        },
-        nullptr, nullptr};
-    static constexpr WGPULoggingCallbackInfo kDefaultLoggingCallbackInfo = {
-        nullptr,
-        [](WGPULoggingType, WGPUStringView, void*, void*) {
-            static bool calledOnce = false;
-            if (!calledOnce) {
-                calledOnce = true;
-                dawn::WarningLog() << "No Dawn device logging callback callback was set. This is "
-                                      "probably not intended. If you really want to ignore logs "
-                                      "and suppress this message, set the callback explicitly.";
-            }
-        },
-        nullptr, nullptr};
-#else
-    static constexpr WGPUUncapturedErrorCallbackInfo kDefaultUncapturedErrorCallbackInfo =
-        kEmptyUncapturedErrorCallbackInfo;
-    static constexpr WGPULoggingCallbackInfo kDefaultLoggingCallbackInfo =
-        kEmptyLoggingCallbackInfo;
-#endif  // DAWN_ENABLE_ASSERTS
-
-    mUncapturedErrorCallbackInfo = kDefaultUncapturedErrorCallbackInfo;
-    if (descriptor->uncapturedErrorCallbackInfo.callback != nullptr) {
-        mUncapturedErrorCallbackInfo = descriptor->uncapturedErrorCallbackInfo;
-    }
-
-    mLoggingCallbackInfo = kDefaultLoggingCallbackInfo;
-
     AdapterInfo adapterInfo;
     adapter->APIGetInfo(&adapterInfo);
 
@@ -573,11 +504,7 @@
 
     // Reset callbacks since after dropping the last external reference, the application may have
     // freed any device-scope memory needed to run the callback.
-    mUncapturedErrorCallbackInfo = kEmptyUncapturedErrorCallbackInfo;
-    {
-        std::lock_guard<std::shared_mutex> lock(mLoggingMutex);
-        mLoggingCallbackInfo = kEmptyLoggingCallbackInfo;
-    }
+    mCallbackInfos.Clear();
 
     GetInstance()->RemoveDevice(this);
 
@@ -823,15 +750,13 @@
     if (forwardToErrorScope == ForwardToErrorScope::Yes) {
         captured = GetErrorScopeStack()->HandleError(ToWGPUErrorType(type), messageStr);
     }
-
-    // Only call the uncaptured error callback if the device is alive. After the
-    // device is lost, the uncaptured error callback should cease firing.
-    if (!captured && mUncapturedErrorCallbackInfo.callback != nullptr && mState == State::Alive) {
-        auto device = ToAPI(this);
-        mUncapturedErrorCallbackInfo.callback(
-            &device, ToAPI(ToWGPUErrorType(type)), ToOutputStringView(messageStr),
-            mUncapturedErrorCallbackInfo.userdata1, mUncapturedErrorCallbackInfo.userdata2);
+    if (captured || mState != State::Alive) {
+        return;
     }
+
+    auto device = ToAPI(this);
+    mCallbackInfos.CallErrorCallback(&device, ToAPI(ToWGPUErrorType(type)),
+                                     ToOutputStringView(messageStr));
 }
 
 void DeviceBase::HandleErrorGeneratingAsyncTask(Ref<ErrorGeneratingAsyncTask> task,
@@ -870,11 +795,10 @@
 }
 
 void DeviceBase::APISetLoggingCallback(const WGPULoggingCallbackInfo& callbackInfo) {
-    if (mState != State::Alive) {
+    if (mState != State::Alive || callbackInfo.callback == nullptr) {
         return;
     }
-    std::lock_guard<std::shared_mutex> lock(mLoggingMutex);
-    mLoggingCallbackInfo = callbackInfo;
+    mCallbackInfos.SetLoggingCallbackInfo(callbackInfo);
 }
 
 ErrorScopeStack* DeviceBase::GetErrorScopeStack() {
@@ -1952,16 +1876,7 @@
 }
 
 void DeviceBase::EmitLog(wgpu::LoggingType type, std::string_view message) {
-    // Acquire a shared lock. This allows multiple threads to emit logs,
-    // or even logs to be emitted re-entrantly. It will block if there is a call
-    // to SetLoggingCallback. Applications should not call SetLoggingCallback inside
-    // the logging callback or they will deadlock.
-    std::shared_lock<std::shared_mutex> lock(mLoggingMutex);
-    if (mLoggingCallbackInfo.callback) {
-        mLoggingCallbackInfo.callback(ToAPI(type), ToOutputStringView(message),
-                                      mLoggingCallbackInfo.userdata1,
-                                      mLoggingCallbackInfo.userdata2);
-    }
+    mCallbackInfos.CallLoggingCallback(ToAPI(type), ToOutputStringView(message));
 }
 
 wgpu::Status DeviceBase::APIGetAHardwareBufferProperties(void* handle,
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index 50ce8e7..a03ffda 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -44,6 +44,7 @@
 #include "dawn/common/RefCountedWithExternalCount.h"
 #include "dawn/common/StackAllocated.h"
 #include "dawn/common/ThreadLocal.h"
+#include "dawn/common/WGPUDeviceCallbackInfos.h"
 #include "dawn/native/AsyncTask.h"
 #include "dawn/native/CacheKey.h"
 #include "dawn/native/Commands.h"
@@ -620,11 +621,7 @@
                                                     const TextureCopy& dst,
                                                     const Extent3D& copySizePixels) = 0;
 
-    WGPUUncapturedErrorCallbackInfo mUncapturedErrorCallbackInfo =
-        WGPU_UNCAPTURED_ERROR_CALLBACK_INFO_INIT;
-
-    std::shared_mutex mLoggingMutex;
-    WGPULoggingCallbackInfo mLoggingCallbackInfo = WGPU_LOGGING_CALLBACK_INFO_INIT;
+    WGPUDeviceCallbackInfos mCallbackInfos;
 
     // Error scopes need to be thread local, but also need to be cleaned up when the device is
     // destroyed. To do this, we can't use thread_local natively because we wouldn't have a way to
diff --git a/src/dawn/wire/client/Device.cpp b/src/dawn/wire/client/Device.cpp
index 9d3c9c4..c430429 100644
--- a/src/dawn/wire/client/Device.cpp
+++ b/src/dawn/wire/client/Device.cpp
@@ -156,78 +156,8 @@
                             EventType::CreateRenderPipeline,
                             WGPUCreateRenderPipelineAsyncCallbackInfo>;
 
-// Default callback infos depending on the build type.
-#ifdef DAWN_ENABLE_ASSERTS
-static constexpr WGPUDeviceLostCallbackInfo kDefaultDeviceLostCallbackInfo = {
-    nullptr, WGPUCallbackMode_AllowSpontaneous,
-    [](WGPUDevice const*, WGPUDeviceLostReason, WGPUStringView, void*, void*) {
-        static std::once_flag flag;
-        std::call_once(flag, []() {
-            dawn::WarningLog() << "No Dawn device lost callback was set. This is probably not "
-                                  "intended. If you really want to ignore device lost "
-                                  "and suppress this message, set the callback explicitly.";
-        });
-    },
-    nullptr, nullptr};
-static constexpr WGPUUncapturedErrorCallbackInfo kDefaultUncapturedErrorCallbackInfo = {
-    nullptr,
-    [](WGPUDevice const*, WGPUErrorType, WGPUStringView, void*, void*) {
-        static std::once_flag flag;
-        std::call_once(flag, []() {
-            dawn::WarningLog() << "No Dawn device uncaptured error callback was set. This is "
-                                  "probably not intended. If you really want to ignore errors "
-                                  "and suppress this message, set the callback explicitly.";
-        });
-    },
-    nullptr, nullptr};
-static constexpr WGPULoggingCallbackInfo kDefaultLoggingCallbackInfo = {
-    nullptr,
-    [](WGPULoggingType, WGPUStringView, void*, void*) {
-        static std::once_flag flag;
-        std::call_once(flag, []() {
-            dawn::WarningLog() << "No Dawn device logging callback callback was set. This is "
-                                  "probably not intended. If you really want to ignore logs "
-                                  "and suppress this message, set the callback explicitly.";
-        });
-    },
-    nullptr, nullptr};
-#else
-static constexpr WGPUDeviceLostCallbackInfo kDefaultDeviceLostCallbackInfo = {
-    nullptr, WGPUCallbackMode_AllowSpontaneous, nullptr, nullptr, nullptr};
-static constexpr WGPUUncapturedErrorCallbackInfo kDefaultUncapturedErrorCallbackInfo = {
-    nullptr, nullptr, nullptr, nullptr};
-static constexpr WGPULoggingCallbackInfo kDefaultLoggingCallbackInfo = {nullptr, nullptr, nullptr,
-                                                                        nullptr};
-#endif  // DAWN_ENABLE_ASSERTS
-
-const WGPUDeviceLostCallbackInfo& GetDeviceLostCallbackInfo(
-    const WGPUDeviceDescriptor* descriptor) {
-    if (descriptor != nullptr && descriptor->deviceLostCallbackInfo.callback != nullptr) {
-        return descriptor->deviceLostCallbackInfo;
-    }
-    return kDefaultDeviceLostCallbackInfo;
-}
-
-const WGPUUncapturedErrorCallbackInfo& GetUncapturedErrorCallbackInfo(
-    const WGPUDeviceDescriptor* descriptor) {
-    if (descriptor != nullptr && descriptor->uncapturedErrorCallbackInfo.callback != nullptr) {
-        return descriptor->uncapturedErrorCallbackInfo;
-    }
-    return kDefaultUncapturedErrorCallbackInfo;
-}
-
 }  // namespace
 
-Device::CallbackInfos::CallbackInfos(const WGPUUncapturedErrorCallbackInfo& error,
-                                     const WGPULoggingCallbackInfo& logging) {
-    if (error.callback != nullptr) {
-        this->error = error;
-    }
-    if (logging.callback != nullptr) {
-        this->logging = logging;
-    }
-}
-
 class Device::DeviceLostEvent : public TrackedEvent {
   public:
     static constexpr EventType kType = EventType::DeviceLost;
@@ -258,16 +188,10 @@
             mMessage = "A valid external Instance reference no longer exists.";
         }
 
-        mDevice->mCallbackInfos.Use<NotifyType::None>([](auto callbackInfos) {
-            callbackInfos->error = std::nullopt;
-            callbackInfos->logging = std::nullopt;
-
-            // The uncaptured error and logging callbacks are spontaneous and must not be called
-            // after we call the device lost's |mCallback| below. Although we have cleared those
-            // callbacks, we need to wait for any remaining outstanding callbacks to finish before
-            // continuing.
-            callbackInfos.Wait([](auto& x) { return x.semaphore == 0; });
-        });
+        // The uncaptured error and logging callbacks are spontaneous and must not be called
+        // after we call the device lost's |mCallback| below, so we clear them and wait for them to
+        // be no longer referenced before moving forwards.
+        mDevice->mCallbackInfos.Clear();
 
         void* userdata1 = mUserdata1.ExtractAsDangling();
         void* userdata2 = mUserdata2.ExtractAsDangling();
@@ -295,8 +219,9 @@
                Adapter* adapter,
                const WGPUDeviceDescriptor* descriptor)
     : RefCountedWithExternalCount<ObjectWithEventsBase>(params, eventManagerHandle),
-      mDeviceLostInfo(AcquireRef(new DeviceLostEvent(GetDeviceLostCallbackInfo(descriptor), this))),
-      mCallbackInfos(GetUncapturedErrorCallbackInfo(descriptor), kDefaultLoggingCallbackInfo),
+      mDeviceLostInfo(
+          AcquireRef(new DeviceLostEvent(GetDeviceLostCallbackInfoOrDefault(descriptor), this))),
+      mCallbackInfos(descriptor),
       mAdapter(adapter) {}
 
 ObjectType Device::GetObjectType() const {
@@ -361,53 +286,12 @@
 }
 
 void Device::HandleError(WGPUErrorType errorType, WGPUStringView message) {
-    std::optional<WGPUUncapturedErrorCallbackInfo> callbackInfo;
-    mCallbackInfos.Use<NotifyType::None>([&](auto callbackInfos) {
-        callbackInfo = callbackInfos->error;
-        if (callbackInfo) {
-            callbackInfos->semaphore += 1;
-        }
-    });
-
-    // If we don't have a callback info, we can just return.
-    if (!callbackInfo) {
-        return;
-    }
-
-    // Call the callback without holding the lock to prevent any re-entrant issues.
-    DAWN_ASSERT(callbackInfo->callback != nullptr);
     const auto device = ToAPI(this);
-    callbackInfo->callback(&device, errorType, message, callbackInfo->userdata1,
-                           callbackInfo->userdata2);
-
-    mCallbackInfos.Use([&](auto callbackInfos) {
-        DAWN_ASSERT(callbackInfos->semaphore > 0);
-        callbackInfos->semaphore -= 1;
-    });
+    mCallbackInfos.CallErrorCallback(&device, errorType, message);
 }
 
 void Device::HandleLogging(WGPULoggingType loggingType, WGPUStringView message) {
-    std::optional<WGPULoggingCallbackInfo> callbackInfo;
-    mCallbackInfos.Use<NotifyType::None>([&](auto callbackInfos) {
-        callbackInfo = callbackInfos->logging;
-        if (callbackInfo) {
-            callbackInfos->semaphore += 1;
-        }
-    });
-
-    // If we don't have a callback info, we can just return.
-    if (!callbackInfo) {
-        return;
-    }
-
-    // Call the callback without holding the lock to prevent any re-entrant issues.
-    DAWN_ASSERT(callbackInfo->callback != nullptr);
-    callbackInfo->callback(loggingType, message, callbackInfo->userdata1, callbackInfo->userdata2);
-
-    mCallbackInfos.Use([&](auto callbackInfos) {
-        DAWN_ASSERT(callbackInfos->semaphore > 0);
-        callbackInfos->semaphore -= 1;
-    });
+    mCallbackInfos.CallLoggingCallback(loggingType, message);
 }
 
 void Device::HandleDeviceLost(WGPUDeviceLostReason reason, WGPUStringView message) {
@@ -429,8 +313,7 @@
 
 void Device::APISetLoggingCallback(const WGPULoggingCallbackInfo& callbackInfo) {
     if (mIsAlive) {
-        mCallbackInfos.Use<NotifyType::None>(
-            [&](auto callbackInfos) { callbackInfos->logging = callbackInfo; });
+        mCallbackInfos.SetLoggingCallbackInfo(callbackInfo);
     }
 }
 
diff --git a/src/dawn/wire/client/Device.h b/src/dawn/wire/client/Device.h
index 1d3130e..3d2f6ad 100644
--- a/src/dawn/wire/client/Device.h
+++ b/src/dawn/wire/client/Device.h
@@ -34,8 +34,8 @@
 #include <optional>
 
 #include "dawn/common/LinkedList.h"
-#include "dawn/common/MutexProtected.h"
 #include "dawn/common/RefCountedWithExternalCount.h"
+#include "dawn/common/WGPUDeviceCallbackInfos.h"
 #include "dawn/wire/WireCmd_autogen.h"
 #include "dawn/wire/client/ApiObjects_autogen.h"
 #include "dawn/wire/client/LimitsAndFeatures.h"
@@ -106,22 +106,7 @@
     LimitsAndFeatures mLimitsAndFeatures;
     std::variant<Ref<TrackedEvent>, FutureID> mDeviceLostInfo;
 
-    struct CallbackInfos {
-        CallbackInfos(const WGPUUncapturedErrorCallbackInfo& error,
-                      const WGPULoggingCallbackInfo& logging);
-
-        // The callback infos are optional because once the device is lost, they are set to
-        // std::nullopt and no longer do anything.
-        std::optional<WGPUUncapturedErrorCallbackInfo> error = std::nullopt;
-        std::optional<WGPULoggingCallbackInfo> logging = std::nullopt;
-
-        // Counter that tracks how many places are currently using callback infos. This is used to
-        // ensure that before we call the device lost callback (which may deallocate the uncaptured
-        // error and logging callbacks), we have ensured that there are no outstanding references to
-        // those callbacks.
-        uint32_t semaphore = 0;
-    };
-    MutexCondVarProtected<CallbackInfos> mCallbackInfos;
+    WGPUDeviceCallbackInfos mCallbackInfos;
 
     Ref<Adapter> mAdapter;
     Ref<Queue> mQueue;