Skip to main content

rs_teststand_sys/
dispatch.rs

1//! Late-bound COM dispatch: the seam between safe wrappers and live COM.
2//!
3//! [`Dispatch`] is the trait every wrapper talks to. The real implementation
4//! [`ComDispatch`] drives an `IDispatch` via `Invoke`; tests substitute a fake
5//! that implements the same trait, so wrapper logic runs with no COM at all.
6//!
7//! Every `VARIANT` here is held as an [`OwnedVariant`], which clears itself on
8//! drop, including on the error paths, where a manual clear is easy to forget.
9
10use std::fmt;
11
12use windows::Win32::Globalization::LOCALE_USER_DEFAULT;
13use windows::Win32::System::Com::{
14    CLSCTX_ALL, CLSIDFromProgID, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx,
15    CoUninitialize, DISPATCH_FLAGS, DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT,
16    DISPPARAMS, EXCEPINFO, IDispatch,
17};
18use windows::Win32::System::Variant::VARIANT;
19use windows_core::{GUID, HSTRING};
20
21use crate::error::ComError;
22use crate::value::Value;
23use crate::variant::OwnedVariant;
24
25/// The named-argument dispatch id a property put must supply
26/// (`DISPID_PROPERTYPUT`).
27const DISPID_PROPERTYPUT: i32 = -3;
28
29/// The late-bound call surface a wrapper needs from a COM object.
30///
31/// Kept deliberately small; it grows a member only when a wrapper first needs
32/// one, each with its own test.
33pub trait Dispatch: fmt::Debug {
34    /// Reads a property by dispatch id (`DISPATCH_PROPERTYGET`, no arguments).
35    ///
36    /// # Errors
37    /// [`ComError::Hresult`] if the COM call fails, or
38    /// [`ComError::UnexpectedType`] if the returned value has an unmodeled type.
39    fn get(&self, dispid: i32) -> Result<Value, ComError>;
40
41    /// Sets a property by dispatch id (`DISPATCH_PROPERTYPUT`).
42    ///
43    /// Required, deliberately: a default body returning "not implemented" would
44    /// be a stub that silently turns a missing implementation into a runtime
45    /// error. Making it required turns that into a compile error instead.
46    ///
47    /// # Errors
48    /// [`ComError::Hresult`] if the COM call fails.
49    fn put(&self, dispid: i32, value: Value) -> Result<(), ComError>;
50
51    /// Invokes a method by dispatch id (`DISPATCH_METHOD`) with arguments.
52    ///
53    /// Required for the same reason as [`Dispatch::put`].
54    ///
55    /// # Errors
56    /// [`ComError::Hresult`] if the COM call fails, or
57    /// [`ComError::UnexpectedType`] if the returned value has an unmodeled type.
58    fn call(&self, dispid: i32, args: &[Value]) -> Result<Value, ComError>;
59
60    /// An owned handle to the same COM object, when there is one.
61    ///
62    /// COM interface pointers are reference counted, so duplicating one is a
63    /// refcount bump rather than a copy of the object. This exists because
64    /// passing an object *back* to the engine needs an owned handle, and a
65    /// caller normally only has a borrow.
66    ///
67    /// Returns `None` for test fakes, which have no COM identity to share.
68    fn duplicate(&self) -> Option<Box<dyn Dispatch>> {
69        None
70    }
71
72    /// The underlying `IDispatch`, when this really is a live COM object.
73    ///
74    /// Returns `None` for test fakes, which is what stops a fake from being
75    /// marshalled into a `VARIANT` and handed to the engine.
76    fn as_idispatch(&self) -> Option<&IDispatch> {
77        None
78    }
79}
80
81/// A live COM object addressed through its `IDispatch` interface.
82#[derive(Debug, Clone)]
83pub struct ComDispatch(IDispatch);
84
85impl ComDispatch {
86    /// Wraps an existing `IDispatch` (e.g. a nested object returned by a call).
87    #[must_use]
88    pub const fn new(dispatch: IDispatch) -> Self {
89        Self(dispatch)
90    }
91
92    /// Single funnel for every `IDispatch::Invoke`.
93    ///
94    /// Centralising it means the exception-unwrapping rule (below) and the
95    /// result's ownership are implemented once instead of per call shape.
96    fn invoke(
97        &self,
98        dispid: i32,
99        flags: DISPATCH_FLAGS,
100        params: &DISPPARAMS,
101        context: &'static str,
102    ) -> Result<OwnedVariant, ComError> {
103        let mut result = OwnedVariant::empty();
104        let mut exception = EXCEPINFO::default();
105        let mut arg_error = 0u32;
106
107        // SAFETY: `params` is a well-formed DISPPARAMS whose argument array (if
108        // any) outlives this call; `result`, `exception` and `arg_error` are
109        // valid owned out-params; IID_NULL is the required riid for late
110        // binding. `result` owns whatever the callee writes into it and clears
111        // it on drop, including on the error path below.
112        let status = unsafe {
113            self.0.Invoke(
114                dispid,
115                &GUID::zeroed(),
116                LOCALE_USER_DEFAULT,
117                flags,
118                &raw const *params,
119                Some(result.as_mut_ptr()),
120                Some(&raw mut exception),
121                Some(&raw mut arg_error),
122            )
123        };
124
125        if let Err(error) = status {
126            // A DISP_E_EXCEPTION merely says "the callee raised"; the engine's
127            // real error code is in EXCEPINFO.scode. It is zero for every other
128            // failure, so prefer it only when set.
129            let code = if exception.scode == 0 {
130                error.code().0
131            } else {
132                exception.scode
133            };
134            return Err(ComError::member(code, context, dispid));
135        }
136
137        Ok(result)
138    }
139}
140
141impl Dispatch for ComDispatch {
142    fn duplicate(&self) -> Option<Box<dyn Dispatch>> {
143        Some(Box::new(Self(self.0.clone())))
144    }
145
146    fn as_idispatch(&self) -> Option<&IDispatch> {
147        Some(&self.0)
148    }
149
150    fn get(&self, dispid: i32) -> Result<Value, ComError> {
151        let no_args = DISPPARAMS::default();
152        self.invoke(
153            dispid,
154            DISPATCH_PROPERTYGET,
155            &no_args,
156            "IDispatch::Invoke (get)",
157        )?
158        .to_value()
159    }
160
161    fn put(&self, dispid: i32, value: Value) -> Result<(), ComError> {
162        let mut argument = OwnedVariant::from_value(&value)?;
163        let mut put_dispid = DISPID_PROPERTYPUT;
164        // A property put passes its value as the single named argument
165        // DISPID_PROPERTYPUT; `argument` outlives the call.
166        let params = DISPPARAMS {
167            rgvarg: argument.as_mut_ptr(),
168            rgdispidNamedArgs: &raw mut put_dispid,
169            cArgs: 1,
170            cNamedArgs: 1,
171        };
172        self.invoke(
173            dispid,
174            DISPATCH_PROPERTYPUT,
175            &params,
176            "IDispatch::Invoke (put)",
177        )?;
178        Ok(())
179    }
180
181    fn call(&self, dispid: i32, args: &[Value]) -> Result<Value, ComError> {
182        // COM reads rgvarg in reverse order. Building the whole vector before
183        // the call means a conversion failure part-way through simply drops the
184        // already-built variants, each clearing itself.
185        let mut arguments = args
186            .iter()
187            .rev()
188            .map(OwnedVariant::from_value)
189            .collect::<Result<Vec<_>, _>>()?;
190
191        let count = u32::try_from(arguments.len())
192            .map_err(|_| ComError::hresult(-2_147_024_809, "argument count exceeds COM limit"))?;
193
194        let params = DISPPARAMS {
195            // `OwnedVariant` is `#[repr(transparent)]` over `VARIANT`, so the
196            // slice is layout-compatible with the `VARIANT` array COM expects.
197            rgvarg: arguments.as_mut_ptr().cast::<VARIANT>(),
198            rgdispidNamedArgs: std::ptr::null_mut(),
199            cArgs: count,
200            cNamedArgs: 0,
201        };
202
203        let result = self.invoke(
204            dispid,
205            DISPATCH_METHOD | DISPATCH_PROPERTYGET,
206            &params,
207            "IDispatch::Invoke (call)",
208        )?;
209        // `arguments` stays alive until here, then each variant clears itself.
210        drop(arguments);
211        result.to_value()
212    }
213}
214
215/// Creates a COM object from a `ProgID` and returns it as a dispatch handle.
216///
217/// # Errors
218/// [`ComError::Hresult`] if the apartment cannot be initialized, the `ProgID` is
219/// unknown, or the class cannot be instantiated.
220pub fn create_dispatch(prog_id: &str) -> Result<ComDispatch, ComError> {
221    init_apartment()?;
222    let prog_id = HSTRING::from(prog_id);
223
224    // SAFETY: `prog_id` is a valid, live wide string for the duration of the call.
225    let clsid = unsafe { CLSIDFromProgID(&prog_id) }
226        .map_err(|error| ComError::hresult(error.code().0, "CLSIDFromProgID"))?;
227
228    // SAFETY: `clsid` is a valid CLSID; no aggregation (None); the requested
229    // interface `IDispatch` matches the returned type parameter.
230    let dispatch: IDispatch = unsafe { CoCreateInstance(&raw const clsid, None, CLSCTX_ALL) }
231        .map_err(|error| ComError::hresult(error.code().0, "CoCreateInstance"))?;
232
233    Ok(ComDispatch::new(dispatch))
234}
235
236/// Initializes COM on the current thread as a single-threaded apartment.
237///
238/// Idempotent: `S_FALSE` (already initialized on this thread) is success.
239/// Never paired with `CoUninitialize` here, uninitializing COM while engine
240/// objects are alive aborts the process; let COM unwind at thread exit.
241///
242/// # Errors
243/// [`ComError::Hresult`] if `CoInitializeEx` reports a hard failure.
244pub fn init_apartment() -> Result<(), ComError> {
245    // SAFETY: standard per-thread COM initialization; the returned HRESULT is
246    // inspected rather than assumed, and S_FALSE is treated as success.
247    let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
248    // RPC_E_CHANGED_MODE means the thread already belongs to the other
249    // concurrency model. That is a deliberate choice by whoever set the thread
250    // up, not a failure, a host may run the engine on a multithreaded thread.
251    if result.is_ok() || result.0 == RPC_E_CHANGED_MODE {
252        Ok(())
253    } else {
254        Err(ComError::hresult(result.0, "CoInitializeEx"))
255    }
256}
257
258/// `RPC_E_CHANGED_MODE`: this thread is already in the other apartment model.
259const RPC_E_CHANGED_MODE: i32 = -2_147_417_850;
260
261/// Uninitializes COM on the current thread, balancing [`init_apartment`].
262///
263/// # When this is needed, and when it is a mistake
264///
265/// A thread that initializes an apartment and then **exits** should uninitialize
266/// first. The process's main thread never really has to: the process is ending
267/// anyway. A spawned thread does, it genuinely detaches while the runtime still
268/// believes it owns a live apartment.
269///
270/// Ordering is the precondition, so this takes the object rather than trusting
271/// the caller to have dropped it: `last` is released here, and only then is the
272/// apartment closed. Uninitializing COM while an object is still alive aborts
273/// the process, which is why there is no bare "uninitialize" to misuse.
274pub fn close_apartment(last: Box<dyn Dispatch>) {
275    drop(last);
276    // SAFETY: the only COM handle this function can be given has just been
277    // dropped, so the apartment holds no live reference from this crate.
278    unsafe { CoUninitialize() };
279}