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