From 25010218be57947fb60cf135c43be9753d8d7abe Mon Sep 17 00:00:00 2001 From: Tatsuyuki Ishi Date: Sat, 7 Aug 2021 00:15:40 +0900 Subject: [PATCH] wasapi: Allow both threading models and switch the default to STA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit COM can prevent undefined behavior in either concurrency model by performing marshaling when necessary. As a result, CoInitializeEx can be called with either concurrency model, and in this case STA provides better compatibility with other code requiring STA, e.g. ASIO backend or winit drag-and-drop. To dive into the detail, the entry point of WASAPI, MMDeviceEnumerator, is registered with "both" threading model, which means that COM objects are created with whatever the thread's concurrency model is set to. This raises the concern that when STA is used, marshaling might make audio buffer operations block on the main thread, breaking continuous audio processing. However, the implementation actually uses free-threaded marshaller for interfaces dealing with buffer operations, which effectively bypasses COM's compatibility marshaling behavior and perform any API calls on the caller's thread instead. Therefore, the interfaces would operate just fine on either concurrency model. For more details on COM's threading model, see [1]. [1] https://thrysoee.dk/InsideCOM+/ch04d.htm Co-Authored-By: Henrik RydgÄrd Close #348 Close #504 Close #538 --- src/host/wasapi/com.rs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/host/wasapi/com.rs b/src/host/wasapi/com.rs index 1b1d109cc..8eb00f34b 100644 --- a/src/host/wasapi/com.rs +++ b/src/host/wasapi/com.rs @@ -1,17 +1,30 @@ //! Handles COM initialization and cleanup. -use super::check_result; +use super::IoError; use std::ptr; use super::winapi::um::combaseapi::{CoInitializeEx, CoUninitialize}; -use super::winapi::um::objbase::COINIT_MULTITHREADED; +use super::winapi::um::objbase::{COINIT_APARTMENTTHREADED}; +use super::winapi::shared::winerror::{SUCCEEDED, HRESULT, RPC_E_CHANGED_MODE}; thread_local!(static COM_INITIALIZED: ComInitialized = { unsafe { - // this call can fail if another library initialized COM in single-threaded mode - // handling this situation properly would make the API more annoying, so we just don't care - check_result(CoInitializeEx(ptr::null_mut(), COINIT_MULTITHREADED)).unwrap(); - ComInitialized(ptr::null_mut()) + // Try to initialize COM with STA by default to avoid compatibility issues with the ASIO + // backend (where CoInitialize() is called by the ASIO SDK) or winit (where drag and drop + // requires STA). + // this call can fail with RPC_E_CHANGED_MODE if another library initialized COM with MTA. + // That's OK though since COM ensures thread-safety/compatibility through marshalling when + // necessary. + let result = CoInitializeEx(ptr::null_mut(), COINIT_APARTMENTTHREADED); + if SUCCEEDED(result) || result == RPC_E_CHANGED_MODE { + ComInitialized { + result, + _ptr: ptr::null_mut(), + } + } else { + // COM initialization failed in another way, something is really wrong. + panic!("Failed to initialize COM: {}", IoError::from_raw_os_error(result)); + } } }); @@ -19,12 +32,19 @@ thread_local!(static COM_INITIALIZED: ComInitialized = { /// // We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the // object. -struct ComInitialized(*mut ()); +struct ComInitialized { + result: HRESULT, + _ptr: *mut (), +} impl Drop for ComInitialized { #[inline] fn drop(&mut self) { - unsafe { CoUninitialize() }; + // Need to avoid calling CoUninitialize() if CoInitializeEx failed since it may have + // returned RPC_E_MODE_CHANGED - which is OK, see above. + if SUCCEEDED(self.result) { + unsafe { CoUninitialize() }; + } } }