microsoft/windows-rs

Clarification of the lifecycle of COM implementations

Closed this issue · 4 comments

Suggestion

I'm building a custom COM event handler using the implement macro, however I'm finding myself lost regarding the lifetime/ownership of said implementation. It's easiest to explain by referring to an example

Example

Cargo.toml
[package]
name = "param_example"
version = "0.1.0"
edition = "2021"

[target.'cfg(windows)'.dependencies]
windows-core = { version = "0.58" }
windows = { version = "0.58", features = [
    "Win32_System_Com",
    "Win32_UI_Accessibility",
    "UI_UIAutomation",
    "implement",
] }
use windows::Win32::{
    System::Com::{CoCreateInstance, CoIncrementMTAUsage, CLSCTX_ALL},
    UI::Accessibility::{
        CUIAutomation, IUIAutomation, IUIAutomationElement, IUIAutomationFocusChangedEventHandler,
        IUIAutomationFocusChangedEventHandler_Impl,
    },
};
use windows_core::implement;

#[implement(IUIAutomationFocusChangedEventHandler)]
pub struct AutomationFocusChangedEventHandler(Box<dyn Fn(&IUIAutomationElement)>);

impl AutomationFocusChangedEventHandler {
    fn new(handler: Box<dyn Fn(&IUIAutomationElement)>) -> Self {
        Self(handler)
    }
}

impl IUIAutomationFocusChangedEventHandler_Impl for AutomationFocusChangedEventHandler_Impl {
    fn HandleFocusChangedEvent(
        &self,
        sender: Option<&IUIAutomationElement>,
    ) -> windows_core::Result<()> {
        if let Some(sender) = sender {
            self.0(sender);
            // println!("Focus changed: {:?}", sender);
        } else {
            println!("HandleFocusChangedEvent had no sender");
        }
        Ok(())
    }
}

fn main() {
    let _ = unsafe { CoIncrementMTAUsage().unwrap() };
    let automation: IUIAutomation =
        unsafe { CoCreateInstance(&CUIAutomation, None, CLSCTX_ALL).unwrap() };

    // Create custom handler
    let handler: IUIAutomationFocusChangedEventHandler =
        AutomationFocusChangedEventHandler::new(Box::new(|sender: &IUIAutomationElement| {
            println!("Focus changed: {:?}", sender);
        }))
        .into();

    unsafe {
        automation
            .AddFocusChangedEventHandler(None, &handler)
            .unwrap()
    };
    // Explicitly drop the custom handler object
    drop(handler);

    // Keep the program running until user input
    println!("Click around your screen to trigger the focus change event handler. Press Enter to exit...");
    let mut buffer = String::new();
    std::io::stdin().read_line(&mut buffer).unwrap();
}

"Where" is the Box<dyn Fn(&IUIAutomationElement)>?

The crux of the program is that I create this custom IUIAutomationFocusChangedEventHandler object (handler), pass a reference to AddFocusChangedEventHandler(&handler), and then drop(handler).

My initial instinct is to expect this program to crash whenever the focus changes -- I've dropped the handler, and it is ostensibly the owner of the Box<dyn Fn(&IUIAutomationElement)> that is ultimately called by the event callback. If that memory has been dropped, then I should crash when the event handler tries to call it. However instead the program functions fine.

I believe what may be happening is that when I convert the custom AutomationFocusChangedEventHandler into() an IUIAutomationFocusChangedEventHandler, ownership of that Box<dyn Fn(&IUIAutomationElement)> is taken by IUIAutomationFocusChangedEventHandler, and ultimately managed for me by the generated code, which in turn manages it via COM reference counting. However I was unable to confirm this one way or another; the implement macro is difficult to parse, and I'm not sure precisely how into() is being implemented.

Thread safety

Another thought I have is that in the case of this particular event API, the handler can be called from another thread, and might be called concurrently/in-parallel (e.g. if two focus changes happen in quick succession, and the second triggers the handler before the first has had a chance to finish executing). Therefore in rust terms, my custom

#[implement(IUIAutomationFocusChangedEventHandler)]
pub struct AutomationFocusChangedEventHandler(Box<dyn Fn(&IUIAutomationElement)>);

ought to be Send + Sync.

Granted this API is unsafe, and so the responsibility is on me to ensure that I use it safely given it's underlying semantics, however I'd like to try and confirm my understanding here (relates to #3171).

May relate to #3036

COM interfaces, implemented by the implement macro or otherwise, provide intrinsic reference counting to manage lifetime. When you drop some value that represents a COM interface, all you're doing is releasing the particular reference count owned by that value. When all references are released, the object "drops" itself and reclaims whatever memory it occupied.

Here's a deep dive on how this works: https://www.pluralsight.com/courses/com-essentials

Thanks @kennykerr, I may well end up taking that course to bootstrap my COM knowledge for this current project. Your explanation makes sense.

Whenever you have a spare moment I'm curious on your thoughts regarding the Send + Sync piece of my question. This may be merely an idiosyncrasy of the particular API I'm using.

Understanding Threading Issues states.

image

It's not entirely clear, but that at least suggests the possibility that if I pass a IUIAutomationFocusChangedEventHandler in one thread, it may be sent to another (Send) and called from there.

It also states

image

which might imply (given that MTA is assumed for this API) that such event handlers can be called in parallel.

Would determining the fact of these matters and enforcing them be within the scope of your work here? Or are these facets of the Rust <--> COM programming model considered to be within the domain of the API caller (me)?

I think I addressed that part of your question here: #3171 (comment)

I think I addressed that part of your question here: #3171 (comment)

Perhaps you did and I'm just too ignorant to see how. As I see it currently, this question is slightly different.

To summarize and maybe clarify: when I call a function like IUIAutomation:: AddFocusChangedEventHandler , it takes a IUIAutomationFocusChangedEventHandler (let's call it handler) as one of the parameters. That handler is then owned by the OS, and called by it whenever the given event (focus change) occurs.

My question relates to the case where I want to create my own custom IUIAutomationFocusChangedEventHandler by doing something along the lines of a

#[implement(IUIAutomationFocusChangedEventHandler)]
pub struct AutomationFocusChangedEventHandler;

I'm arguing that, given the nature of AddFocusChangedEventHandler, shouldn't my custom #[implement(IUIAutomationFocusChangedEventHandler)] be forced to be Send + Sync? The reason being, the OS now owns that object, and from what I can tell from the documentation it may decide to use it in a Send or Sync manner (for example, the OS might call it from a thread besides the one that I called uiautomation.AddFocusChangedEventHandler(handler) from, which implies Send).