Simplify WinRT and COM class authoring
Dedicating an issue to this topic. Originally part of #81 (huge thread).
The windows crate now supports implementing COM and WinRT interfaces, but more is required to support classes as as whole. This early sample illustrates what's possible today:
https://github.com/kennykerr/component-rs
I still have much work to do to streamline this experience.
@kennykerr Is it possible today to author WinRT components in Rust, either using windows-rs crate or not? π€
I suppose it would require the use of Midl compiler + writing COM wiring manually. Could you maybe provide a sample of what can be done today?
Thanks!
You can do so today but yes you'd need to write some IDL and call the MIDL compiler to produce a winmd. Next year I'm hoping to spend most of my time on improving authoring support and being able to produce a winmd directly from Rust so that IDL will not be required.
Thank you for the quick response! π
I don't mind writing IDL and calling Midl compiler manually. Having an ability to author WinRT components would unblock the use of Rust for me π
Do I understand correctly that I would need to call the windows::build! macro on the resulting .winmd's interfaces, and then #[implement] them? π€
And implement an IActivationFactory and DllGetActivationFactory of course.
Yep, that's it. It's a bit error-prone which is also why I'm planning on automating much of that.
Yeah, I can imagine π I'm gonna try this out and if I succeed, I could open a PR to the Samples repo, if you would be interested π
Sounds good!
Hi @kennykerr , you might be interested to take a look:
https://github.com/Alovchin91/winrt-component-rs
Please let me know if you have any suggestions, comments etc. π
I'll also open a bunch of issues to share my experience with windows-bindgen and how in my opinion it could be improved π
Thanks @Alovchin91, that's very helpful. I'm now starting to work on this and #1093 in earnest. We should get to a point where MIDL is no longer required.
Synced a bit offline, just want to leave a comment here, please also consider authoring event and delegate, prototype a winrt "event" type (similar like c++/winrt winrt::event struct ) so when implement the event, we can know how to manage event handler and token. Thanks!
@hmyan90 take a look at #1705 - that should address your immediate need.
0.36.0 has been released and includes the new Event<T> type.
Any update to this task? Looking forward to this so much!
I'm hard at work maturing support for component authoring. You can already implement components, with some limitations. There is an example here that you can use as a starting point:
https://github.com/microsoft/windows-rs/tree/master/crates/tests/component
Trying to understand this, I'm coming from com-rs crate, which was deprecated in favor of windows-rs, and it had this kind of macros:
#[com_interface("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
and
#[co_class(implements(IVirtualDesktopNotification))]
Is the test component example somehow the same thing but without macros? I can't find similar types of macro shortcuts in there, maybe it's more manual now?
Usage examples in com-rs
Defining interface
#[com_interface("CD403E52-DEED-4C13-B437-B98380F2B1E8")]
pub trait IVirtualDesktopNotification: IUnknown {
unsafe fn virtual_desktop_created(
&self,
monitors: ComRc<dyn IObjectArray>,
desktop: ComRc<dyn IVirtualDesktop>,
) -> HRESULT;
unsafe fn virtual_desktop_destroy_begin(
&self,
monitors: ComRc<dyn IObjectArray>,
desktopDestroyed: ComRc<dyn IVirtualDesktop>,
desktopFallback: ComRc<dyn IVirtualDesktop>,
) -> HRESULT;
// ...
}
and implementing class for it
#[co_class(implements(IVirtualDesktopNotification))]
struct VirtualDesktopChangeListener {
sender: Mutex<Option<VirtualDesktopEventSender>>,
}
impl IVirtualDesktopNotification for VirtualDesktopChangeListener {
/// On desktop creation
unsafe fn virtual_desktop_created(
&self,
_monitors: ComRc<dyn IObjectArray>,
idesktop: ComRc<dyn IVirtualDesktop>,
) -> HRESULT {
HRESULT::ok()
}
/// On desktop destroy begin
unsafe fn virtual_desktop_destroy_begin(
&self,
_monitors: ComRc<dyn IObjectArray>,
_destroyed_desktop: ComRc<dyn IVirtualDesktop>,
_fallback_desktop: ComRc<dyn IVirtualDesktop>,
) -> HRESULT {
HRESULT::ok()
}
// ...
}
Above are snipptes from my code I have a lots of code written with com-rs crate.
I'm trying to reimplement these in windows-rs, but I can't find similar examples as in com-rs crate had.
I'm in the middle of developing first-class component authoring support, hence the lack of docs and samples but you can take this example as a guide. That happens to be a WinRT component but the same pattern applies for COM components. COM factories just implement IClassFactory instead of IActivationFactory and export DllGetClassObject instead of DllGetActivationFactory.
By the way, your IVirtualDesktopNotification looks a lot like IVirtualDesktopManager which means you don't have to define it yourself and can just use the definitions provided by the windows crate.
I got it working with similar macros: windows_interface::interface and windows::core::implement
What I don't get is this instruction to use ManuallyDrop whenever there is _In_ IFooBar* then use ManuallyDrop<IFooBar>... that didn't work for me, I just got a lot of problems that way.
#[windows_interface::interface("CD403E52-DEED-4C13-B437-B98380F2B1E8")]
pub unsafe trait IVirtualDesktopNotification: IUnknown {
unsafe fn virtual_desktop_created(
&self,
monitors: IObjectArray, // If I use ManuallyDrop<IObjectArray> it doesn't feel right here? It works without
desktop: IVirtualDesktop,
) -> HRESULT;
unsafe fn virtual_desktop_destroy_begin(
&self,
monitors: IObjectArray,
desktopDestroyed: IVirtualDesktop,
desktopFallback: IVirtualDesktop,
) -> HRESULT;
// ...
}
And implementation:
#[windows::core::implement(IVirtualDesktopNotification)]
struct VirtualDesktopNotification {}
impl IVirtualDesktopNotification_Impl for VirtualDesktopNotification {
unsafe fn virtual_desktop_created(
&self,
monitors: IObjectArray,
desktop: IVirtualDesktop,
) -> HRESULT {
HRESULT(0)
}
unsafe fn virtual_desktop_destroy_begin(
&self,
monitors: IObjectArray,
desktopDestroyed: IVirtualDesktop,
desktopFallback: IVirtualDesktop,
) -> HRESULT {
HRESULT(0)
}
// ....
}
Youβre passing IObjectArray by value vs. by reference (&IObjectArray) which means youβre moving it into the function and the function now owns the value. When the function exits, it drops the value, which means the reference counter is decremented (IUnknown.Release is called). By using ManuallyDrop<IObjectArray> youβre essentially saying that the function should not drop the value. I believe an alternative option would be to .clone() your object array before passing it to the function β this way you get an IUnknown.AddRef call beforehand which is then matched by the Release call when the value is dropped.
@Alovchin91 Windows calls those functions, I give the instance to some register API.
If I've understood COM correctly the caller increments before calling, and the one using it releases at the end. So it should work without ManuallyDrops?
@Ciantic That seems to agree with how the [in] attribute is documented. If my understanding is correct, using the ManuallyDrop<T> wrapper here would ultimately leak the object implementing the interface.
That leaves me wondering, though...
- whether COM actually has an attribute to describe a "borrow"...
- and if it doesn't whether this table is accurate.
I'm leaning that the table is not accurate. We should not use ManuallyDrop if COM API works as it should. However, a bigger example in the official FAQ might be in order.
The use case people need to see is translating C++ to Rust.
My current thought is this:
C++ _In_ IFooBar* in windows-rs is IFooBar
C++ _In_opt_ IFooBar in windows-rs is Option<IFooBar>
C++ _Out_ IFooBar** in windows-rs is *mut Option<IFooBar>
C++ MIDL_INTERFACE("B2F925B9-5A0F-4D2E-9F4D-2B1507593C10") is windows-rs #[windows_interface::interface("b2f925b9-5a0f-4d2e-9f4d-2b1507593c10")]
Additionally example of windows::core::implement
MIDL_INTERFACE("B2F925B9-5A0F-4D2E-9F4D-2B1507593C10")
IVirtualDesktopManagerInternal : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetCount(
_In_opt_ HMONITOR monitor,
_Out_ UINT* pCount) = 0;
virtual HRESULT STDMETHODCALLTYPE MoveViewToDesktop(
_In_ IApplicationView* pView,
_In_ IVirtualDesktop* pDesktop) = 0;
virtual HRESULT STDMETHODCALLTYPE CanViewMoveDesktops(
_In_ IApplicationView* pView,
_Out_ BOOL* pfCanViewMoveDesktops) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCurrentDesktop(
_In_opt_ HMONITOR monitor,
_Out_ IVirtualDesktop** desktop) = 0;
virtual HRESULT STDMETHODCALLTYPE GetAllCurrentDesktops(
_Out_ IObjectArray** ppDesktops) = 0;
virtual HRESULT STDMETHODCALLTYPE GetDesktops(
_In_opt_ HMONITOR monitor,
_Out_ IObjectArray** ppDesktops) = 0;
virtual HRESULT STDMETHODCALLTYPE GetAdjacentDesktop(
_In_ IVirtualDesktop* pDesktopReference,
_In_ AdjacentDesktop uDirection,
_Out_ IVirtualDesktop** ppAdjacentDesktop) = 0;
virtual HRESULT STDMETHODCALLTYPE SwitchDesktop(
_In_opt_ HMONITOR monitor,
_In_ IVirtualDesktop* pDesktop) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateDesktopW(
_In_opt_ HMONITOR monitor,
_Out_ IVirtualDesktop** ppNewDesktop) = 0;
// ...
}
I think it translates to this:
#[windows_interface::interface("b2f925b9-5a0f-4d2e-9f4d-2b1507593c10")]
pub unsafe trait IVirtualDesktopManagerInternal: IUnknown {
unsafe fn get_count(&self, monitor: Option<HMONITOR>, outCount: *mut UINT) -> HRESULT;
unsafe fn move_view_to_desktop(
&self,
view: IApplicationView,
desktop: IVirtualDesktop,
) -> HRESULT;
unsafe fn can_move_view_between_desktops(
&self,
view: IApplicationView,
canMove: *mut i32,
) -> HRESULT;
unsafe fn get_current_desktop(
&self,
monitor: HMONITOR,
outDesktop: *mut Option<IVirtualDesktop>,
) -> HRESULT;
unsafe fn get_all_current_desktops(&self, outDesktops: *mut Option<IObjectArray>) -> HRESULT;
unsafe fn get_desktops(
&self,
monitor: HMONITOR,
outDesktops: *mut Option<IObjectArray>,
) -> HRESULT;
unsafe fn get_adjacent_desktop(
&self,
inDesktop: IVirtualDesktop,
direction: UINT,
out_pp_desktop: *mut Option<IVirtualDesktop>,
) -> HRESULT;
unsafe fn switch_desktop(&self, monitor: HMONITOR, desktop: IVirtualDesktop) -> HRESULT;
unsafe fn create_desktop(
&self,
monitor: HMONITOR,
outDesktop: *mut Option<IVirtualDesktop>,
) -> HRESULT;
// ...
}
This is slightly confusing as a COM interface pointer is modeled as a value type in Rust. If you think of it in terms of C++ it makes a little more sense conceptually. An input parameter passes the raw pointer into the function. The caller ensures that the pointer is stable for the duration of the synchronous call and the callee depends on that assurance, but the caller does not transfer ownership to the callee. Rust models it more like a C++ smart pointer, but such abstractions are not valid on the ABI where the parameter must ultimately be the equivalent of a raw C++ pointer. This is why @wesleywiser correctly suggests using ManuallyDrop.
Anyway, I still plan to make this a lot simpler and safer in Rust. If you want to understand how this all works, I explain all of this and much more in great detail here:
https://www.pluralsight.com/courses/com-essentials
The caller ensures that the pointer is stable for the duration of the synchronous call and the callee depends on that assurance, but the caller does not transfer ownership to the callee.
This does sound like clone() on call or ManuallyDrop is required.
I have used ComPtr in C++ successfully, and ComRc and ComPtr in com-rs crate. I guess something like that would be nice in here too, now this feels pretty error-prone, but I think if I can remember to clone each time this works.
I actually have the book Essential COM, Don Box it feels like I have enough COM info for my lifetime, maybe your content is more succinct.
Keep in mind that COM relies on the stdcall calling convention (or 64-bit equivalents) which requires the caller to pack the stack and the callee to unpack the stack. What that means is that if the caller passes an object with destructor, such as a Drop implementation, by value then the compiler will assume the callee will either assume ownership or drop the value. But that is not what COM expects so if you're doing that on your end (e.g. passing a cloned value) you're going to cause a COM reference leak when the callee is written in something other than Rust.
Now I'm just getting clever.
virtual HRESULT STDMETHODCALLTYPE SwitchDesktop(
_In_opt_ HMONITOR monitor,
_In_ IVirtualDesktop* pDesktop) = 0;
For that _In_ I made this
// Behaves like ManuallyDrop but is kept alive for as long as the given
// reference
#[repr(transparent)]
pub struct ComIn<'a, T: Vtable> {
data: *mut c_void,
_phantom: std::marker::PhantomData<&'a T>,
}
impl<'a, T: Vtable> ComIn<'a, T> {
pub fn new(t: &'a T) -> Self {
Self {
// Copies the raw Inteface pointer
data: t.as_raw(),
_phantom: std::marker::PhantomData,
}
}
}
impl<'a, T: Vtable> Deref for ComIn<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { std::mem::transmute(&self.data) }
}
}
And use it in interface definitions like this:
unsafe fn switch_desktop(&self, monitor: HMONITOR, desktop: ComIn<IVirtualDesktop>) -> HRESULT;
And call it like this:
unsafe {
manager
.switch_desktop(0, ComIn::new(¤t_desk))
.unwrap()
};
NOTE I think the table is correct, but here is it again:
C++ InOpt, In, Out and OutOpt equivalents in Rust
- InOpt =
Option<ComIn<IMyObject>>orOption<ManuallyDrop<IMyObject>> - In =
ComIn<IMyObject>orManuallyDrop<IMyObject> - Out =
*mut Option<IMyObject> - OutOpt =
*mut Option<IMyObject>
Last two are same intentionally.
The summary of COM object lifetime rules:
When a COM object is passed from caller to callee as an input parameter to a method, the caller is expected to keep a reference on the object for the duration of the method call. The callee shouldn't need to call
AddReforReleasefor the synchronous duration of that method call.When a COM object is passed from callee to caller as an out parameter from a method the object is provided to the caller with a reference already taken and the caller owns the reference. Which is to say, it is the caller's responsibility to call
Releasewhen they're done with the object.When making a copy of a COM object pointer you need to call
AddRefandRelease. TheAddRefmust be called before you callReleaseon the original COM object pointer.
Rules as written by David Risney.
I'm not sure if this is the right place, but I want to prevent duplicated issues.
Basically, I want to define host objects in webviw2, but it requires types have IDispatch interface.
And searching a bit, it seems it'll be lots of work to bring components manually.
Here's what I would like to achieve:
#[interface("3a14c9c0-bc3e-453f-a314-4ce4a0ec81d8")]
unsafe trait IHostObjectSample: IDispatch {
fn greet(&self, name: BSTR) -> BSTR;
}
#[implement(IHostObjectSample)]
struct HostObjectSample {}
impl IHostObjectSample_Impl for HostObjectSample {
unsafe fn greet(&self, name: BSTR) -> BSTR {
let wide = name.as_wide();
BSTR::from_wide(&wide).unwrap()
}
}
But it seems it's not possible for now, and it'll need to wait for authoring support. Or is there a way to implement IDispatch for now?
You should be able to implement IDispatch in this scenario. Be sure to include the necessary feature requirements:
[dependencies.windows]
version = "0.46.0"
features = [
"implement",
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Ole",
]
Then you just need to include an implementation:
impl IDispatch_Impl for HostObjectSample {
fn GetTypeInfoCount(&self) -> Result<u32> { todo!() }
fn GetTypeInfo(&self, _: u32, _: u32) -> Result<ITypeInfo> { todo!() }
fn GetIDsOfNames(&self, _: *const GUID, _: *const PCWSTR, _: u32, _: u32, _: *mut i32) -> Result<()> { todo!() }
fn Invoke(&self, _: i32, _: *const GUID, _: u32, _: DISPATCH_FLAGS, _: *const DISPPARAMS, _: *mut VARIANT, _: *mut EXCEPINFO, _: *mut u32) -> Result<()> { todo!() }
}
@kennykerr would you please provide a simple IDispatch example? I found a c++ version in stackoverflow, but a simpler example of Rust version would be great.
For WebView2 host objects, you can skip implementing GetTypeInfo and just return Ok(0) from GetTypeInfoCount. This only leaves implementing GetIDsOfNames and Invoke, which is trivial for most cases, only the VARIANTs are a bit annoying.
(Edit: FWIW, I'd check whether this gives better performance than other ways of communicating with your Rust code if performance is what you're aiming for. For the Tauri app that I'm working on, host objects are actually slightly slower than whatever Tauri is doing for its built-in commands, at least for basic in/out objects, but YMMV.)
I wrote a macro called wvwasi_macro::create_type_info_crate that automatically generates a trait for getting ITypeInfo, which simplifies the implementation of GetTypeInfo.
Something changed between 0.53 and 0.56, this code
#[interface("094d70d6-5202-44b8-abb8-43860da5aca2")]
unsafe trait IValue: IUnknown {
fn GetValue(&self, value: *mut i32) -> HRESULT;
}
Began to give an error that it requires a "windows_core" crate, I added windows-core crate and it works again.