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 drop
ped 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.
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
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
).