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.
50 // We pass `COINIT_MULTITHREADED` to join the MTA. The result
51 // is checked below, and `CoUninitialize` is guaranteed via Drop.
52 let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
53
54 if let Err(e) = hr.ok() {
55 tracing::error!(error = ?e, "COM MTA initialization failed");
56 return Err(anyhow::anyhow!("CoInitializeEx failed: {e}"));
57 }
58
59 tracing::debug!("COM MTA initialized");
60
61 Ok(Self {
62 _not_send: PhantomData,
63 })
64 }
65}
66
67impl Drop for ComGuard {
68 fn drop(&mut self) {
69 tracing::debug!("COM MTA teardown");
70 // SAFETY: Paired with the successful `CoInitializeEx` in `new()`.
71 // Construction guarantees COM was initialized, so this call is
72 // always balanced. Only runs on the creating thread (!Send).
73 unsafe {
74 CoUninitialize();
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn com_guard_constructs_and_drops() {
85 // On Windows, CoInitializeEx(MTA) should succeed.
86 // On non-Windows CI, this test is skipped by target gate.
87 let guard = ComGuard::new();
88 assert!(guard.is_ok(), "ComGuard::new() should succeed: {guard:?}");
89 // Guard drops here — CoUninitialize runs.
90 }
91}