windows-rs icon indicating copy to clipboard operation
windows-rs copied to clipboard

Simplify WinRT and COM class authoring

Open kennykerr opened this issue 4 years ago β€’ 36 comments

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 avatar Aug 26 '21 19:08 kennykerr

@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!

Alovchin91 avatar Dec 08 '21 22:12 Alovchin91

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.

kennykerr avatar Dec 08 '21 23:12 kennykerr

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.

Alovchin91 avatar Dec 08 '21 23:12 Alovchin91

Yep, that's it. It's a bit error-prone which is also why I'm planning on automating much of that.

kennykerr avatar Dec 08 '21 23:12 kennykerr

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 πŸ™‚

Alovchin91 avatar Dec 08 '21 23:12 Alovchin91

Sounds good!

kennykerr avatar Dec 08 '21 23:12 kennykerr

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 πŸ™‚

Alovchin91 avatar Dec 30 '21 07:12 Alovchin91

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.

kennykerr avatar Mar 14 '22 19:03 kennykerr

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!

huiminyan2017 avatar Apr 15 '22 22:04 huiminyan2017

@hmyan90 take a look at #1705 - that should address your immediate need.

kennykerr avatar Apr 19 '22 21:04 kennykerr

0.36.0 has been released and includes the new Event<T> type.

kennykerr avatar Apr 26 '22 18:04 kennykerr

Any update to this task? Looking forward to this so much!

nerocui avatar Nov 03 '22 00:11 nerocui

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

kennykerr avatar Nov 03 '22 03:11 kennykerr

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.

Ciantic avatar Feb 04 '23 00:02 Ciantic

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.

kennykerr avatar Feb 04 '23 20:02 kennykerr

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)
  }
  // ....
}

Ciantic avatar Feb 05 '23 11:02 Ciantic

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 avatar Feb 05 '23 11:02 Alovchin91

@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 avatar Feb 05 '23 12:02 Ciantic

@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...

  1. whether COM actually has an attribute to describe a "borrow"...
  2. and if it doesn't whether this table is accurate.

tim-weis avatar Feb 05 '23 13:02 tim-weis

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

E.g. here is C++
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;
    // ...
}

Ciantic avatar Feb 05 '23 13:02 Ciantic

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

kennykerr avatar Feb 05 '23 16:02 kennykerr

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.

Ciantic avatar Feb 05 '23 18:02 Ciantic

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.

kennykerr avatar Feb 05 '23 19:02 kennykerr

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(&current_desk))
        .unwrap()
};

NOTE I think the table is correct, but here is it again:

C++ InOpt, In, Out and OutOpt equivalents in Rust

  1. InOpt = Option<ComIn<IMyObject>> or Option<ManuallyDrop<IMyObject>>
  2. In = ComIn<IMyObject> or ManuallyDrop<IMyObject>
  3. Out = *mut Option<IMyObject>
  4. OutOpt = *mut Option<IMyObject>

Last two are same intentionally.

The summary of COM object lifetime rules:

  1. 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 AddRef or Release for the synchronous duration of that method call.

  2. 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 Release when they're done with the object.

  3. When making a copy of a COM object pointer you need to call AddRef and Release. The AddRef must be called before you call Release on the original COM object pointer.

Rules as written by David Risney.

Ciantic avatar Feb 05 '23 19:02 Ciantic

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?

wusyong avatar Mar 21 '23 13:03 wusyong

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 avatar Mar 22 '23 15:03 kennykerr

@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.

defims avatar Apr 20 '23 14:04 defims

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

lesderid avatar May 01 '23 22:05 lesderid

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.

defims avatar Mar 18 '24 04:03 defims

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.

Ciantic avatar May 01 '24 16:05 Ciantic