variant_rs/
dispatch.rs

1//! Utilities for using [`IDispatch`] from Rust in an ergonomic fashion
2
3use crate::convert::VariantConversionError;
4use crate::variant::Variant;
5use thiserror::Error;
6use widestring::U16CString;
7use windows::core::{Error as WinError, GUID, PCWSTR};
8use windows::Win32::Foundation::{DISP_E_EXCEPTION, DISP_E_PARAMNOTFOUND, DISP_E_TYPEMISMATCH};
9use windows::Win32::System::Com::{
10    IDispatch, DISPATCH_FLAGS, DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT,
11    DISPPARAMS, EXCEPINFO,
12};
13use windows::Win32::System::Ole::DISPID_PROPERTYPUT;
14use windows::Win32::System::Variant::VARIANT;
15
16pub trait IDispatchExt {
17    fn get(&self, name: &str) -> Result<Variant, IDispatchError>;
18    fn put(&self, name: &str, value: Variant) -> Result<(), IDispatchError>;
19    fn call(&self, name: &str, args: Vec<Variant>) -> Result<Variant, IDispatchError>;
20}
21
22#[derive(Error, Debug)]
23pub enum IDispatchError {
24    #[error("Couldn't convert args to VARIANT")]
25    VariantConversion(#[from] VariantConversionError),
26    #[error("Couldn't convert string to BSTR")]
27    StringConversion(#[from] widestring::error::ContainsNul<u16>),
28    #[error("Win32 error")]
29    GenericWin32(#[from] WinError),
30    #[error("COM exception")]
31    Exception(EXCEPINFO),
32    #[error("COM argument error {0} for argument {1}")]
33    Argument(ComArgumentError, usize),
34}
35
36#[derive(Error, Debug)]
37pub enum ComArgumentError {
38    #[error("The value's type does not match the expected type for the parameter")]
39    TypeMismatch,
40    #[error("A required parameter was missing")]
41    ParameterNotFound,
42}
43
44const LOCALE_USER_DEFAULT: u32 = 0x0400;
45const LOCALE_SYSTEM_DEFAULT: u32 = 0x0800;
46
47fn invoke(
48    obj: &IDispatch,
49    name: &str,
50    dp: &mut DISPPARAMS,
51    flags: DISPATCH_FLAGS,
52) -> Result<Variant, IDispatchError> {
53    let mut name = U16CString::from_str(name).map_err(IDispatchError::StringConversion)?;
54    let mut id = 0i32;
55    unsafe {
56        obj.GetIDsOfNames(
57            &GUID::default(),
58            &PCWSTR(name.as_mut_ptr()),
59            1,
60            LOCALE_USER_DEFAULT,
61            (&mut id) as *mut i32,
62        )
63    }
64    .map_err(IDispatchError::GenericWin32)?;
65
66    let mut excep = EXCEPINFO::default();
67    let mut arg_err = 0;
68    let mut result = VARIANT::default();
69
70    let res = unsafe {
71        obj.Invoke(
72            id,
73            &GUID::default(),
74            LOCALE_SYSTEM_DEFAULT,
75            flags,
76            dp,
77            Some(&mut result),
78            Some(&mut excep),
79            Some(&mut arg_err),
80        )
81    };
82
83    match res {
84        Ok(_) => result.try_into().map_err(Into::into),
85        Err(e) => Err(match e.code() {
86            DISP_E_EXCEPTION => IDispatchError::Exception(excep),
87            DISP_E_TYPEMISMATCH => {
88                IDispatchError::Argument(ComArgumentError::TypeMismatch, arg_err as usize)
89            }
90            DISP_E_PARAMNOTFOUND => {
91                IDispatchError::Argument(ComArgumentError::ParameterNotFound, arg_err as usize)
92            }
93            _ => IDispatchError::GenericWin32(e),
94        }),
95    }
96}
97
98/// Get a property from the COM object
99///
100/// # Example
101/// ```
102/// use variant_rs::get;
103/// let x = get!(com_object, SomeProp)?;
104/// ```
105#[macro_export]
106macro_rules! get {
107    ($obj:expr, $name:ident) => {{
108        use variant_rs::dispatch::IDispatchExt;
109        $obj.get(stringify!($name))
110    }};
111}
112
113/// Set a property on the COM object
114///
115/// # Example
116/// ```
117/// use variant_rs::put;
118/// put!(com_object, SomeProp, 10)?;
119/// ```
120#[macro_export]
121macro_rules! put {
122    ($obj:expr, $name:ident, $value:expr) => {{
123        use variant_rs::dispatch::IDispatchExt;
124        let val: Variant = $value.into();
125        $obj.put(stringify!($name), val)
126    }};
127}
128
129/// Call a method on the COM object
130///
131/// # Example
132/// ```
133/// use variant_rs::call;
134/// let x = call!(com_object, SomeMethod(10, "hello"))?;
135/// ```
136#[macro_export]
137macro_rules! call {
138    ($obj:expr, $name:ident($($arg:expr),*)) => {
139        {
140            use variant_rs::dispatch::IDispatchExt;
141            let args = vec![$((&$arg).into()),*];
142            $obj.call(stringify!($name), args)
143        }
144    };
145}
146
147impl IDispatchExt for IDispatch {
148    /// Get a property from a COM object
149    ///
150    /// Note: consider using the [`get!`] macro
151    fn get(&self, name: &str) -> Result<Variant, IDispatchError> {
152        let mut dp = DISPPARAMS::default();
153        invoke(self, name, &mut dp, DISPATCH_PROPERTYGET)
154    }
155
156    /// Set a property on a COM object
157    ///
158    /// Note: consider using the [`put!`] macro
159    fn put(&self, name: &str, value: Variant) -> Result<(), IDispatchError> {
160        let mut value = value.try_into()?;
161        let mut dp = DISPPARAMS {
162            cArgs: 1,
163            rgvarg: &mut value,
164            cNamedArgs: 1,
165            ..Default::default()
166        };
167        let mut id = DISPID_PROPERTYPUT;
168        dp.rgdispidNamedArgs = &mut id as *mut _;
169        invoke(self, name, &mut dp, DISPATCH_PROPERTYPUT)?;
170        Ok(())
171    }
172
173    /// Call a method on a COM object
174    ///
175    /// Note: consider using the [`call!`] macro
176    fn call(&self, name: &str, args: Vec<Variant>) -> Result<Variant, IDispatchError> {
177        let mut dp = DISPPARAMS::default();
178        let args: Vec<VARIANT> = args
179            .into_iter()
180            .rev()
181            .map(|v| v.try_into().map_err(IDispatchError::from))
182            .collect::<Result<_, _>>()?;
183        dp.cArgs = args.len() as u32;
184        dp.rgvarg = args.as_ptr() as *mut _;
185        invoke(self, name, &mut dp, DISPATCH_METHOD)
186    }
187}