Skip to main content

opc_da_client/
com_guard.rs

1//! RAII guard for COM initialization/teardown.
2//!
3//! Ensures `CoUninitialize` is called exactly once per successful
4//! `CoInitializeEx`, even on early returns or panics.
5
6use std::marker::PhantomData;
7use windows::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx, CoUninitialize};
8
9/// Drop guard for COM thread initialization.
10///
11/// Calling [`ComGuard::new`] initializes COM in Multi-Threaded Apartment
12/// (MTA) mode. When the guard is dropped, `CoUninitialize` is called
13/// automatically.
14///
15/// # Thread Safety
16///
17/// `ComGuard` is intentionally `!Send` and `!Sync`. COM initialization
18/// is per-thread — the guard **must** be created and dropped on the same
19/// OS thread. This is enforced at compile time.
20///
21/// # Examples
22///
23/// ```no_run
24/// # use anyhow::Result;
25/// # use opc_da_client::ComGuard;
26/// # fn main() -> Result<()> {
27/// let _guard = ComGuard::new()?;
28/// // ... COM operations ...
29/// // CoUninitialize called automatically on drop
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug)]
34pub struct ComGuard {
35    /// Prevents `Send + Sync` auto-derivation. COM init is per-thread.
36    _not_send: PhantomData<*mut ()>,
37}
38
39impl ComGuard {
40    /// Initialize COM in Multi-Threaded Apartment (MTA) mode.
41    ///
42    /// Returns `Ok(ComGuard)` on success (including `S_FALSE`, which
43    /// means COM was already initialized on this thread).
44    ///
45    /// # Errors
46    ///
47    /// Returns `Err` if `CoInitializeEx` fails with a fatal HRESULT.
48    pub fn new() -> anyhow::Result<Self> {
49        // SAFETY: CoInitializeEx is a standard Win32 FFI call passing COINIT_MULTITHREADED to join MTA.
50        // SAFETY: Result is checked below, and CoUninitialize is guaranteed via Drop.
51        let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
52
53        if let Err(e) = hr.ok() {
54            tracing::error!(error = ?e, "COM MTA initialization failed");
55            return Err(anyhow::anyhow!("CoInitializeEx failed: {e}"));
56        }
57
58        tracing::debug!("COM MTA initialized");
59
60        Ok(Self {
61            _not_send: PhantomData,
62        })
63    }
64}
65
66impl Drop for ComGuard {
67    fn drop(&mut self) {
68        tracing::debug!("COM MTA teardown");
69        // SAFETY: Paired with the successful CoInitializeEx in new().
70        // SAFETY: Construction guarantees COM was initialized, so this call is always balanced. Only runs on the creating thread (!Send).
71        unsafe {
72            CoUninitialize();
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn com_guard_constructs_and_drops() {
83        // On Windows, CoInitializeEx(MTA) should succeed.
84        // On non-Windows CI, this test is skipped by target gate.
85        let guard = ComGuard::new();
86        assert!(guard.is_ok(), "ComGuard::new() should succeed: {guard:?}");
87        // Guard drops here — CoUninitialize runs.
88    }
89}