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 ¶ms,
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 ¶ms,
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}