Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify WinRT and COM class authoring #1094

Open
kennykerr opened this issue Aug 26, 2021 · 31 comments
Open

Simplify WinRT and COM class authoring #1094

kennykerr opened this issue Aug 26, 2021 · 31 comments
Labels
enhancement New feature or request

Comments

@kennykerr
Copy link
Collaborator

kennykerr commented Aug 26, 2021

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.

@Alovchin91
Copy link
Contributor

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

@kennykerr
Copy link
Collaborator Author

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.

@Alovchin91
Copy link
Contributor

Alovchin91 commented Dec 8, 2021

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.

@kennykerr
Copy link
Collaborator Author

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

@Alovchin91
Copy link
Contributor

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 🙂

@kennykerr
Copy link
Collaborator Author

Sounds good!

@Alovchin91
Copy link
Contributor

Alovchin91 commented Dec 30, 2021

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 🙂

@kennykerr
Copy link
Collaborator Author

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.

@hmyan90
Copy link

hmyan90 commented Apr 15, 2022

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!

@kennykerr
Copy link
Collaborator Author

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

@kennykerr
Copy link
Collaborator Author

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

@nerocui
Copy link
Member

nerocui commented Nov 3, 2022

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

@kennykerr
Copy link
Collaborator Author

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

@Ciantic
Copy link

Ciantic commented Feb 4, 2023

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.

@kennykerr
Copy link
Collaborator Author

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.

@Ciantic
Copy link

Ciantic commented Feb 5, 2023

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

@Alovchin91
Copy link
Contributor

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.

@Ciantic
Copy link

Ciantic commented Feb 5, 2023

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

@tim-weis
Copy link
Contributor

tim-weis commented Feb 5, 2023

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

@Ciantic
Copy link

Ciantic commented Feb 5, 2023

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

@kennykerr
Copy link
Collaborator Author

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

@Ciantic
Copy link

Ciantic commented Feb 5, 2023

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.

@kennykerr
Copy link
Collaborator Author

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.

@Ciantic
Copy link

Ciantic commented Feb 5, 2023

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.

@wusyong
Copy link

wusyong commented Mar 21, 2023

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?

@kennykerr
Copy link
Collaborator Author

kennykerr commented Mar 22, 2023

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!() }
}

@defims
Copy link

defims commented Apr 20, 2023

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

@lesderid
Copy link

lesderid commented May 1, 2023

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

@kennykerr kennykerr changed the title Support WinRT and COM class authoring Simplify WinRT and COM class authoring Sep 12, 2023
@defims
Copy link

defims commented Mar 18, 2024

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.

@Ciantic
Copy link

Ciantic commented May 1, 2024

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.

@kennykerr
Copy link
Collaborator Author

#2917

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

9 participants