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::{BSTR, GUID, HSTRING};
20
21use crate::error::ComError;
22use crate::value::{OutKind, 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    /// Calls a member that reports part of its answer through by-reference
61    /// arguments.
62    ///
63    /// The `outputs` describe slots appended after `args`, in declaration
64    /// order. Returns the member's own result together with what it wrote into
65    /// each slot.
66    ///
67    /// # Errors
68    /// [`ComError`] if the call fails, or [`ComError::UnexpectedType`] on a
69    /// dispatch that has no COM identity to call through.
70    fn call_with_outputs(
71        &self,
72        dispid: i32,
73        args: &[Value],
74        outputs: &[OutKind],
75    ) -> Result<(Value, Vec<Value>), ComError> {
76        let _ = (dispid, args, outputs);
77        Err(ComError::UnexpectedType {
78            expected: "a live COM object",
79            actual: "a dispatch with no by-reference support",
80        })
81    }
82
83    /// An owned handle to the same COM object, when there is one.
84    ///
85    /// COM interface pointers are reference counted, so duplicating one is a
86    /// refcount bump rather than a copy of the object. This exists because
87    /// passing an object *back* to the engine needs an owned handle, and a
88    /// caller normally only has a borrow.
89    ///
90    /// Returns `None` for test fakes, which have no COM identity to share.
91    fn duplicate(&self) -> Option<Box<dyn Dispatch>> {
92        None
93    }
94
95    /// The underlying `IDispatch`, when this really is a live COM object.
96    ///
97    /// Returns `None` for test fakes, which is what stops a fake from being
98    /// marshalled into a `VARIANT` and handed to the engine.
99    fn as_idispatch(&self) -> Option<&IDispatch> {
100        None
101    }
102}
103
104/// A live COM object addressed through its `IDispatch` interface.
105#[derive(Debug, Clone)]
106pub struct ComDispatch(IDispatch);
107
108impl ComDispatch {
109    /// Wraps an existing `IDispatch` (e.g. a nested object returned by a call).
110    #[must_use]
111    pub const fn new(dispatch: IDispatch) -> Self {
112        Self(dispatch)
113    }
114
115    /// Single funnel for every `IDispatch::Invoke`.
116    ///
117    /// Centralising it means the exception-unwrapping rule (below) and the
118    /// result's ownership are implemented once instead of per call shape.
119    fn invoke(
120        &self,
121        dispid: i32,
122        flags: DISPATCH_FLAGS,
123        params: &DISPPARAMS,
124        context: &'static str,
125    ) -> Result<OwnedVariant, ComError> {
126        let mut result = OwnedVariant::empty();
127        let mut exception = EXCEPINFO::default();
128        let mut arg_error = 0u32;
129
130        // SAFETY: `params` is a well-formed DISPPARAMS whose argument array (if
131        // any) outlives this call; `result`, `exception` and `arg_error` are
132        // valid owned out-params; IID_NULL is the required riid for late
133        // binding. `result` owns whatever the callee writes into it and clears
134        // it on drop, including on the error path below.
135        let status = unsafe {
136            self.0.Invoke(
137                dispid,
138                &GUID::zeroed(),
139                LOCALE_USER_DEFAULT,
140                flags,
141                &raw const *params,
142                Some(result.as_mut_ptr()),
143                Some(&raw mut exception),
144                Some(&raw mut arg_error),
145            )
146        };
147
148        if let Err(error) = status {
149            // A DISP_E_EXCEPTION merely says "the callee raised"; the engine's
150            // real error code is in EXCEPINFO.scode. It is zero for every other
151            // failure, so prefer it only when set.
152            let code = if exception.scode == 0 {
153                error.code().0
154            } else {
155                exception.scode
156            };
157            return Err(ComError::member(code, context, dispid));
158        }
159
160        Ok(result)
161    }
162}
163
164impl Dispatch for ComDispatch {
165    fn duplicate(&self) -> Option<Box<dyn Dispatch>> {
166        Some(Box::new(Self(self.0.clone())))
167    }
168
169    fn as_idispatch(&self) -> Option<&IDispatch> {
170        Some(&self.0)
171    }
172
173    fn get(&self, dispid: i32) -> Result<Value, ComError> {
174        let no_args = DISPPARAMS::default();
175        self.invoke(
176            dispid,
177            DISPATCH_PROPERTYGET,
178            &no_args,
179            "IDispatch::Invoke (get)",
180        )?
181        .to_value()
182    }
183
184    fn put(&self, dispid: i32, value: Value) -> Result<(), ComError> {
185        let mut argument = OwnedVariant::from_value(&value)?;
186        let mut put_dispid = DISPID_PROPERTYPUT;
187        // A property put passes its value as the single named argument
188        // DISPID_PROPERTYPUT; `argument` outlives the call.
189        let params = DISPPARAMS {
190            rgvarg: argument.as_mut_ptr(),
191            rgdispidNamedArgs: &raw mut put_dispid,
192            cArgs: 1,
193            cNamedArgs: 1,
194        };
195        self.invoke(
196            dispid,
197            DISPATCH_PROPERTYPUT,
198            &params,
199            "IDispatch::Invoke (put)",
200        )?;
201        Ok(())
202    }
203
204    fn call(&self, dispid: i32, args: &[Value]) -> Result<Value, ComError> {
205        // COM reads rgvarg in reverse order. Building the whole vector before
206        // the call means a conversion failure part-way through simply drops the
207        // already-built variants, each clearing itself.
208        let mut arguments = args
209            .iter()
210            .rev()
211            .map(OwnedVariant::from_value)
212            .collect::<Result<Vec<_>, _>>()?;
213
214        let count = u32::try_from(arguments.len())
215            .map_err(|_| ComError::hresult(-2_147_024_809, "argument count exceeds COM limit"))?;
216
217        let params = DISPPARAMS {
218            // `OwnedVariant` is `#[repr(transparent)]` over `VARIANT`, so the
219            // slice is layout-compatible with the `VARIANT` array COM expects.
220            rgvarg: arguments.as_mut_ptr().cast::<VARIANT>(),
221            rgdispidNamedArgs: std::ptr::null_mut(),
222            cArgs: count,
223            cNamedArgs: 0,
224        };
225
226        let result = self.invoke(
227            dispid,
228            DISPATCH_METHOD | DISPATCH_PROPERTYGET,
229            &params,
230            "IDispatch::Invoke (call)",
231        )?;
232        // `arguments` stays alive until here, then each variant clears itself.
233        drop(arguments);
234        result.to_value()
235    }
236
237    fn call_with_outputs(
238        &self,
239        dispid: i32,
240        args: &[Value],
241        outputs: &[OutKind],
242    ) -> Result<(Value, Vec<Value>), ComError> {
243        let mut slots: Vec<OutSlot> = outputs.iter().copied().map(OutSlot::new).collect();
244
245        let mut arguments = args
246            .iter()
247            .map(OwnedVariant::from_value)
248            .collect::<Result<Vec<_>, _>>()?;
249        arguments.extend(slots.iter_mut().map(OutSlot::as_variant));
250        // COM reads rgvarg back to front, so the whole list is reversed once.
251        arguments.reverse();
252
253        let count = u32::try_from(arguments.len())
254            .map_err(|_| ComError::hresult(-2_147_024_809, "argument count exceeds COM limit"))?;
255        let params = DISPPARAMS {
256            rgvarg: arguments.as_mut_ptr().cast::<VARIANT>(),
257            rgdispidNamedArgs: std::ptr::null_mut(),
258            cArgs: count,
259            cNamedArgs: 0,
260        };
261
262        let result = self.invoke(
263            dispid,
264            DISPATCH_METHOD | DISPATCH_PROPERTYGET,
265            &params,
266            "IDispatch::Invoke (call with outputs)",
267        )?;
268        // The by-reference variants stay alive until the engine has written
269        // through them, which is why this is dropped only now.
270        drop(arguments);
271
272        let written = slots.iter().map(OutSlot::to_value).collect();
273        Ok((result.to_value()?, written))
274    }
275}
276
277/// Storage an engine member writes back through, and the variant pointing at it.
278///
279/// Boxed so the address handed to COM survives the argument vector being built
280/// and reversed. The engine allocates any string itself and hands ownership to
281/// the caller, so the `BSTR` here frees what comes back.
282enum OutSlot {
283    /// A `VT_BYREF | VT_BSTR` slot.
284    Text(Box<BSTR>),
285    /// A `VT_BYREF | VT_I4` slot.
286    Int(Box<i32>),
287    /// A `VT_BYREF | VT_BOOL` slot.
288    Bool(Box<windows::Win32::Foundation::VARIANT_BOOL>),
289}
290
291impl OutSlot {
292    /// An empty slot of the requested kind.
293    fn new(kind: OutKind) -> Self {
294        match kind {
295            OutKind::Text => Self::Text(Box::new(BSTR::new())),
296            OutKind::Int => Self::Int(Box::new(0)),
297            OutKind::Bool => Self::Bool(Box::new(windows::Win32::Foundation::VARIANT_BOOL(0))),
298        }
299    }
300
301    /// The by-reference variant to pass for this slot.
302    ///
303    /// Takes `&mut self` to mark the slot as handed over for writing. The
304    /// variant carries a raw pointer, so the borrow ends when this returns and
305    /// keeping the storage alive across the call stays the caller's job.
306    fn as_variant(&mut self) -> OwnedVariant {
307        // SAFETY: each pointer refers to this slot's own boxed storage, which
308        // outlives the returned variant because the caller keeps `self` alive
309        // across the call and drops the variants first.
310        unsafe {
311            match self {
312                Self::Text(cell) => OwnedVariant::byref_text(&raw mut **cell),
313                Self::Int(cell) => OwnedVariant::byref_i32(&raw mut **cell),
314                Self::Bool(cell) => OwnedVariant::byref_bool(&raw mut **cell),
315            }
316        }
317    }
318
319    /// What the engine wrote into this slot.
320    fn to_value(&self) -> Value {
321        match self {
322            Self::Text(cell) => Value::Str(cell.to_string()),
323            Self::Int(cell) => Value::I32(**cell),
324            // A VARIANT_BOOL is 0 for false and -1 for true; anything non-zero
325            // is true, which is how COM callers are expected to read it.
326            Self::Bool(cell) => Value::Bool(cell.0 != 0),
327        }
328    }
329}
330
331/// Creates a COM object from a `ProgID` and returns it as a dispatch handle.
332///
333/// # Errors
334/// [`ComError::Hresult`] if the apartment cannot be initialized, the `ProgID` is
335/// unknown, or the class cannot be instantiated.
336pub fn create_dispatch(prog_id: &str) -> Result<ComDispatch, ComError> {
337    init_apartment()?;
338    let prog_id = HSTRING::from(prog_id);
339
340    // SAFETY: `prog_id` is a valid, live wide string for the duration of the call.
341    let clsid = unsafe { CLSIDFromProgID(&prog_id) }
342        .map_err(|error| ComError::hresult(error.code().0, "CLSIDFromProgID"))?;
343
344    // SAFETY: `clsid` is a valid CLSID; no aggregation (None); the requested
345    // interface `IDispatch` matches the returned type parameter.
346    let dispatch: IDispatch = unsafe { CoCreateInstance(&raw const clsid, None, CLSCTX_ALL) }
347        .map_err(|error| ComError::hresult(error.code().0, "CoCreateInstance"))?;
348
349    Ok(ComDispatch::new(dispatch))
350}
351
352/// Initializes COM on the current thread as a single-threaded apartment.
353///
354/// Idempotent: `S_FALSE` (already initialized on this thread) is success.
355/// Never paired with `CoUninitialize` here, uninitializing COM while engine
356/// objects are alive aborts the process; let COM unwind at thread exit.
357///
358/// # Errors
359/// [`ComError::Hresult`] if `CoInitializeEx` reports a hard failure.
360pub fn init_apartment() -> Result<(), ComError> {
361    // SAFETY: standard per-thread COM initialization; the returned HRESULT is
362    // inspected rather than assumed, and S_FALSE is treated as success.
363    let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
364    // RPC_E_CHANGED_MODE means the thread already belongs to the other
365    // concurrency model. That is a deliberate choice by whoever set the thread
366    // up, not a failure, a host may run the engine on a multithreaded thread.
367    if result.is_ok() || result.0 == RPC_E_CHANGED_MODE {
368        Ok(())
369    } else {
370        Err(ComError::hresult(result.0, "CoInitializeEx"))
371    }
372}
373
374/// `RPC_E_CHANGED_MODE`: this thread is already in the other apartment model.
375const RPC_E_CHANGED_MODE: i32 = -2_147_417_850;
376
377/// Uninitializes COM on the current thread, balancing [`init_apartment`].
378///
379/// # When this is needed, and when it is a mistake
380///
381/// A thread that initializes an apartment and then **exits** should uninitialize
382/// first. The process's main thread never really has to: the process is ending
383/// anyway. A spawned thread does, it genuinely detaches while the runtime still
384/// believes it owns a live apartment.
385///
386/// Ordering is the precondition, so this takes the object rather than trusting
387/// the caller to have dropped it: `last` is released here, and only then is the
388/// apartment closed. Uninitializing COM while an object is still alive aborts
389/// the process, which is why there is no bare "uninitialize" to misuse.
390pub fn close_apartment(last: Box<dyn Dispatch>) {
391    drop(last);
392    // SAFETY: the only COM handle this function can be given has just been
393    // dropped, so the apartment holds no live reference from this crate.
394    unsafe { CoUninitialize() };
395}