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

Defining and Implementing Win32/COM/WinRT types in Rust #81

Closed
kennykerr opened this issue Apr 27, 2020 · 58 comments
Closed

Defining and Implementing Win32/COM/WinRT types in Rust #81

kennykerr opened this issue Apr 27, 2020 · 58 comments
Labels
enhancement New feature or request

Comments

@kennykerr
Copy link
Collaborator

Authoring support is part of the next major milestone - stay tuned!

@kennykerr kennykerr added the enhancement New feature or request label Apr 27, 2020
@kennykerr kennykerr changed the title Support for authoring WinRT types in Rust Defining and Implementing COM and WinRT types in Rust Aug 5, 2020
@kennykerr
Copy link
Collaborator Author

kennykerr commented Aug 5, 2020

Rust presents both unique challenges and unique opportunities when it comes to creating a language projection for the Windows Runtime. Defining and implementing COM and WinRT types in Rust highlights how this language’s rich metaprogramming facilities allow developers rather ingenious flexibility to hook compilation for both importing and exporting type information.

Today, Rust/WinRT lets you import WinRT types from metadata. The resulting imported types are projected as Rust types that are callable in an idiomatic way. This however only covers consumption of types. Now it's time to explore how Rust/WinRT is being extended to allow COM and WinRT types to be defined in Rust, for local consumption and optionally for export as well, and to allow Rust implementations of both imported or locally defined types.

The problem can be broken down as follows:

  • Declare WinRT and COM types in Rust
  • Implement WinRT and COM classes in Rust
  • Export the metadata representation of those types as a winmd file

Today, a Rust project imports types using either the winrt::import or winrt::build macros. Either way, a set of winmd files are gathered and the aggregate of all types are available for import into the Rust type system. This acts as a sort of cache of metadata that is at the Rust project’s disposal. This is important because the import/build macros don’t know ahead of time what types might need to be implemented by the project. Breaking down the problem like this is useful because you can quickly see how the solution comes together. Locally declared WinRT and COM types are immediately exported to a winmd file that is then added to the metadata cache that is then available to any COM and WinRT implementations within the Rust project. The winmd file can then be shared with other projects as needed.

To complement the winrt import/build macros, Rust/WinRT adds the winrt::implements attribute macro to implement any previously declared COM or WinRT classes and interfaces. This macro draws information from the metadata cache to fill in the scaffolding and boilerplate code necessary to implement the desired types in Rust.

Here is an example of how the build macro might be used (and updated) to both import existing type information and declare new project-specific types.

winrt::build! {
    [import]
    windows::foundation::*
    windows::data::xml::dom::*
 
    [export]
    pub mod microsoft {
        pub mod windows {
            pub struct StructType { x: i32, y: i32 };
 
            pub interface IInterfaceType {
                fn method(&self) -> Result<StructType>;
            }
 
            pub class ClassType : IInterfaceType;
        }
    }
}

Then inside the project, any of the classes may be implemented using the winrt::implements macro:

#[winrt::implements(microsoft::windows::ClassType)]
pub struct ClassType {
    fields: u32
}

impl ClassType {
    pub method(&self) -> Result<StructType> {
        Ok(StructType{ x: 1, y: 2 })
    }
}

Because the type information is available to the implements macro, the developer doesn’t need to tell it which interfaces to implement or which methods to expect from the struct implementation. The implements macro also has enough information to build up the necessary vtable pointers and slots to support the COM object at run time.

The implements macro in its simplest form may be used simply to implement an existing WinRT class:

#[winrt::implements(windows::foundation::Uri)]
pub struct Uri { ... }

The implementation will automatically include the windows::foundation::Uri class’ required interfaces, including IUriRuntimeClass, IUriRuntimeClassWithAbsoluteCanonicalUri, and IStringable in this case. The Uri struct will be expected to provide implementations for the methods of all of these interfaces.

The implements macro may also be used to implement a loose set of interfaces:

#[winrt::implements(IFrameworkViewSource, IFrameworkView)]
pub struct App { ... }

In this case, the struct will implement the indicated interfaces and nothing more. This is useful for implementations that don’t necessarily correspond to a WinRT class. Generic and polymorphic implementations of IVector<T> and other collection interfaces may be implemented in this manner as well.

Finally, the implements macro may be used to implement a combination of classes and interfaces:

#[winrt::implements(windows::foundation::SomeClass, ISomethingWinRT, ISomethingCom)]
pub struct Uri { ... }

In this case, the struct will implement all of the interfaces required by SomeClass as well as the loose interfaces indicated by the attribute.

@kennykerr kennykerr pinned this issue Aug 5, 2020
@robmikh
Copy link
Member

robmikh commented Aug 5, 2020

I think this all sounds great! I have some questions, hopefully they aren't too in the weeds:

  1. The whole exporting bit is very interesting. And I'm assuming that most people who want to export types/metadata will use that path. Do you see that part also handling exporting DllGetActivationFactory and the like? I'm mainly asking because I think versioning might still be easier by authoring an idl, but that's a much narrower scenario (in fact it might only matter to the system). It might be useful to "export" types from existing metadata.

  2. In the last example, where you can implement a COM/non-WinRT interface, I'm guessing that includes arbitrary types that implement the ComInterface trait?

@kennykerr
Copy link
Collaborator Author

kennykerr commented Aug 5, 2020

@robmikh

Yes, export implies both the production of the winmd file directly from the Rust build as well as the DllGetActivationFactory implementation that provides access to all of the public WinRT classes that the Rust project implements, allowing you to create a DLL. You can of course still use IDL and then simply import the resulting winmd file and implement some of the existing WinRT classes. Either way (whether the WinRT class is declared locally or imported) there needs to be a step that gathers up all of the public WinRT classes and exports them via DllGetActivationFactory.

I'd like to support the same kind of versioning support that IDL affords directly from Rust. While I think using IDL is fine, I don't want developers to resort to IDL merely because Rust lacks support for versioned interfaces (or anything else). We should be able to easily use attributes to declare interfaces as being required, static, and activatable interfaces of a given WinRT class just as you can in IDL today with all of the same attributes for versioning, noexcept, overloads, renames, properties, and so on.

The trouble with your second question is that a procedural macro like implements doesn't have access to the compilation unit as a whole that may declare other interfaces via something like a ComInterface trait. It only works as I've described above because the implements macro can look at the metadata that was previously collected and synthesized by the build macro. The assumption for COM interfaces is that they too will be imported or declared via the build macro.

@rylev
Copy link
Contributor

rylev commented Aug 6, 2020

I'm not sure that this can work since it would need to be guaranteed that the winrt::build! macro runs before all the winrt::implements macros which I don't think we can guarantee.

Where does the exported metadata end up? If I take a dependency on a project that exports certain types, how do I read the metadata from that dependency?

Other than that I like idea. Granted I don't think that this type of solution has too much precedent, but I think it largely avoids most of the bad practices you sometimes encounter in proc_macros (e.g., network access).

@kennykerr
Copy link
Collaborator Author

kennykerr commented Aug 6, 2020

@rylev

The assumption is that the build macro runs in a sub crate, as is the norm anyway. That should ensure that it runs before hand. Here's an example.

Exporting metadata (and implementations) produces a WinRT component, not a Rust library crate. The resulting build artifacts would be distributed as a nuget package so that any language, not just Rust, can consume that WinRT component. Of course the implementing Rust crate can still be directly consumed by other Rust crates as a dependency and sidestep the WinRT indirection.

@maan2003
Copy link

This one looks more like Rust.

#[winrt(class)]
pub struct ClassType {
    fields: i32,
}

#[winrt]
impl ClassType {
    pub fn method() {}
}

#[winrt]
pub trait IInterfaceType {
    fn imethod();
}

#[winrt]
impl IInterfaceType for ClassType {
    fn imethod() {}
}

wasm-bindgen also export types to javascript(and typescript), maybe their approach could help.

@rylev
Copy link
Contributor

rylev commented Aug 10, 2020

@maan2003 thanks for the feedback. An issue with this is that Interfaces are not traits and so it might be confusing for users to define a trait that ends up becoming a struct. That's why we decided for the custom syntax in class and interface.

@maan2003
Copy link

I don't have understanding of interfaces then 😅. But declaring the public api separate really looks like C header. To be explicit we could have #[winrt(interface)]

@kennykerr
Copy link
Collaborator Author

@maan2003 the trouble is that would require us to pre-process the entire Rust crate looking for WinRT types since Rust procedural macros don't have have access to the entire AST for the crate but only the token stream that represents the macro input. At any rate, it's generally preferred to design ABI-stable APIs up front where the API is declared and implemented separately. This is also not something you'd need to think too much about unless you were defining a WinRT component.

@kennykerr kennykerr unpinned this issue Aug 15, 2020
@kennykerr
Copy link
Collaborator Author

I'm back from vacation and will be focusing on this issue. Thanks for everyone's patience.

@kylone
Copy link

kylone commented Sep 26, 2020

I don't have understanding of interfaces then 😅. But declaring the public api separate really looks like C header. To be explicit we could have #[winrt(interface)]

I'm doing a bit of research here to understand the vision of what this issue is about. The key insight I had was that Windows has an ABI that's a higher level than the C ABI that's common to so many languages: https://docs.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/interop-winrt-abi

Is this issue about generating a crate that can conform to the Windows Runtime ABI?

@kennykerr
Copy link
Collaborator Author

@kylone This crate already implements the WinRT ABI to the extent that you can, as a consumer or caller, use WinRT components via this binary interface. This issue is about extending that to also act as the producer or callee of such WinRT components by implementing the other side of this binary interface.

@kennykerr kennykerr unpinned this issue Oct 2, 2020
@kennykerr
Copy link
Collaborator Author

Just a quick update. I've done a bunch refactoring and I'm now close to getting support for implementing COM interfaces up and running. Stay tuned, I know this is something that a lot of folks are waiting for. Thanks for your patience.

@kennykerr
Copy link
Collaborator Author

A contrived but fun example illustrating both COM and WinRT interfaces working side-by-side:

#[implement(
    Windows::Foundation::IStringable,
    Windows::Win32::System::WinRT::ISwapChainInterop
)]
struct Thing();

#[allow(non_snake_case)]
impl Thing {
    fn ToString(&self) -> Result<HSTRING> {
        Ok("hello world!".into())
    }

    fn SetSwapChain(&self, unknown: &Option<IUnknown>) -> Result<()> {
        if let Some(unknown) = unknown {
            let s: IStringable = unknown.cast()?;
            println!("{}", s.ToString()?);
        }

        Ok(())
    }
}

fn main() -> Result<()> {
    let stringable: IStringable = Thing().into();

    let interop: ISwapChainInterop = stringable.cast()?;

    unsafe { interop.SetSwapChain(&stringable)? };

    Ok(())
}
C:\git\scratch>cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target\debug\scratch.exe`
hello world!

@kennykerr kennykerr changed the title Defining and Implementing COM and WinRT types in Rust Defining and Implementing Win32/COM/WinRT types in Rust Aug 11, 2021
@bdbai
Copy link
Contributor

bdbai commented Aug 16, 2021

Hi @kennykerr, is it possible to derive a WinRT type from other custom types for now? The use case is that our Application subclass should derive from the custom XamlMetaDataProvider so that Frame.Navigate(typeof(MainPage)) will know how to instantiate a MainPage object. Thanks!

@wravery
Copy link
Collaborator

wravery commented Aug 18, 2021

Little bit of encouragement, I pulled from master and tried #[implement] as a replacement for the COM interfaces in windows-samples-rs/webview2_win32: wravery/windows-samples-rs@3eb3994. Looks a lot better already, IMO. 😃

Bit of feedback, I had to include use bindings::Windows::{self, ...}; (and the same for bindings::Microsoft in this specific case) to get the type name resolution in #[implement] to work with a separate bindings crate. It would be nice if there were a way to avoid that, or to add a module path prefix somewhere in the macro arguments.

@knopp
Copy link

knopp commented Aug 21, 2021

OMG, this actually works now :) @kennykerr, thank you so much!

@kennykerr
Copy link
Collaborator Author

@wravery generally the implement macro expects that you include use bindings::* and then everything should work as expected. I'd like to not require this specific use path but that's beyond what Rust can manage at the moment.

@saschanaz
Copy link

I'd like to not require this specific use path but that's beyond what Rust can manage at the moment.

Is there a related Rust tracking issue for that? 👀

@knopp
Copy link

knopp commented Aug 21, 2021

@kennykerr, I managed to implement all interfaces that I needed except for Windows::Win32::System::Com::IDataObject. It's possible that I'm doing something wrong, but tried it with same method signatures as generated IDataObject but it didn't compile. Some of the errors, i.e.

casting `&*mut STGMEDIUM_abi` as `*const STGMEDIUM_abi` is invalid

would suggest that maybe there is a problem with code generation?

@knopp
Copy link

knopp commented Aug 22, 2021

In case it helps, here's the code:

#[implement(Windows::Win32::System::Com::IDataObject)]
pub struct DataObject {}

#[allow(non_snake_case)]
impl DataObject {
    pub fn new() -> Self {
        Self {}
    }

    fn GetData(&self, pformatetc_in: *mut FORMATETC) -> ::windows::Result<STGMEDIUM> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn GetDataHere(
        &self,
        pformatetc: *mut FORMATETC,
        pmedium: *mut STGMEDIUM,
    ) -> ::windows::Result<()> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn QueryGetData(&self, pformatetc: *mut FORMATETC) -> ::windows::Result<()> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn GetCanonicalFormatEtc(&self, pformatectin: *mut FORMATETC) -> ::windows::Result<FORMATETC> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn SetData<'a>(
        &self,
        pformatetc: *mut FORMATETC,
        pmedium: *mut STGMEDIUM,
        frelease: BOOL,
    ) -> ::windows::Result<()> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn EnumFormatEtc(&self, dwdirection: u32) -> ::windows::Result<IEnumFORMATETC> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn DAdvise<'a>(
        &self,
        pformatetc: *mut FORMATETC,
        advf: u32,
        padvsink: &IAdviseSink,
    ) -> ::windows::Result<u32> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn DUnadvise(&self, dwconnection: u32) -> ::windows::Result<()> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }

    fn EnumDAdvise(&self) -> ::windows::Result<IEnumSTATDATA> {
        Err(Error::fast_error(DATA_E_FORMATETC))
    }
}

And the error message:

error[E0308]: mismatched types
   --> nativeshell\src\shell\platform\win32\drag_com.rs:198:1
    |
198 | #[implement(Windows::Win32::System::Com::IDataObject)]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ in mutability
    |
    = note: expected raw pointer `*mut Com::STGMEDIUM`
                 found reference `&Com::STGMEDIUM`
    = note: this error originates in the attribute macro `implement` (in Nightly builds, run with -Z macro-backtrace for more info)
help: parentheses are required to parse this as an expression
    |
198 | (#[implement(Windows::Win32::System::Com::IDataObject)])
    | +                                                      +

error[E0606]: casting `&*mut STGMEDIUM_abi` as `*const STGMEDIUM_abi` is invalid
   --> nativeshell\src\shell\platform\win32\drag_com.rs:198:1
    |
198 | #[implement(Windows::Win32::System::Com::IDataObject)]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: this error originates in the attribute macro `implement` (in Nightly builds, run with -Z macro-backtrace for more info)

@wravery
Copy link
Collaborator

wravery commented Aug 23, 2021

@knopp I think the issue is the signature for SetData. If the parameter is an in/const type, it will generate a binding that passes it as a reference. The first note is telling you that it can't convert that to a *mut pointer. The second error mentioning the _abi is more of the same.

@kennykerr
Copy link
Collaborator Author

Yeah... please don't use IntoParam. Ever. 😉

That's only for the generated bindings to make it easier to call Windows APIs from Rust. When you are implementing an interface/function in Rust you need to simply use the parameter types directly.

@knopp
Copy link

knopp commented Aug 23, 2021

@kennykerr, @wravery, I removed the IntoParam (I just had it there because I copied the signature from generated window.rs, I wasn't planning on leaving it there :). I did try changing the SetData signature to pass STGMEDIUM by const reference but it didn't make any difference. The STGMEDIUM error is still there. The other method that takes STGMEDIUM (GetDataHere) is out so I assume *mut should be correct, right?

@kennykerr
Copy link
Collaborator Author

Note that there are a few (known) scenarios for COM implementations that are not yet covered. I am working through those now. I'll test IDataObject next.

Unlike WinRT where there is a pretty limited and well-known type system that is easily tested, COM is the wild west of method signatures and providing a high percentage of test coverage is challenging.

@mwcampbell
Copy link

@kennykerr While you're at it, can you please make sure VARIANTs are covered, ideally with a wrapper like the one we already have for BSTR? That will be important for implementing the UI Automation provider interfaces.

@aWeinzierl
Copy link

How are you supposed to deal with generic structs (not interfaces), like the following one?

use bindings::*;
use windows::*;

#[implement(
    Windows::Foundation::IStringable,
)]
pub struct Test<Gen>{
    _item: Gen,
}
error[E0107]: this struct takes 1 generic argument but 0 generic arguments were supplied
  --> src\view_models\router_view_model.rs:9:1
   |
9  | / #[implement(
10 | |     Windows::Foundation::IStringable,
11 | | )]
   | |__^ expected 1 generic argument
   |
note: struct defined here, with 1 generic parameter: `Gen`
  --> src\view_models\router_view_model.rs:12:12
   |
12 | pub struct Test<Gen>{
   |            ^^^^ ---
   = note: this error originates in the attribute macro `implement` (in Nightly builds, run with -Z macro-backtrace for more info)
help: add missing generic argument
   |
9  | #Gen[implement(
   |  ^^^

@agausmann
Copy link

Seems that generics aren't supported in the current implementation. And recovering the concrete type of the generic parameter would definitely be non-trivial, if it's even possible. According to cargo expand, the Rust value is stored and accessed as a type-erased raw pointer.

(Aside: It might be possible if the generic parameter is required to implement Any, then a TypeId could be stored and checked before casting.)

@kennykerr
Copy link
Collaborator Author

Yes, the implement macro doesn't attempt to figure out whether the implementation struct is generic. It does assume it is generic when you implement a generic interface, like IVector<T>, but that's about it.

I do plan to support this eventually.

@kennykerr
Copy link
Collaborator Author

@knopp I've done a bunch of work to improve COM support over the last week or two. You should now be able to implement IDataObject. Here's a test/example: #1092

@kennykerr
Copy link
Collaborator Author

kennykerr commented Aug 26, 2021

I'm going to close this issue in favor of some more targeted issues that I've created to focus on some of the remaining work.

This issue is now just too long and divergent to meaningfully capture specific progress. If you think something important is being lost, feel free to reference that in a new issue.

@1Dragoon
Copy link

If it weren't for this, I probably never would have used the win32 api

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