Windows-rs: Defining and Implementing COM and WinRT types in Rust

Created on 27 Apr 2020  Â·  35Comments  Â·  Source: microsoft/windows-rs

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

enhancement

Most helpful comment

And this just started working:

#[implement(
    extend Windows::UI::Xaml::Application,
    override OnLaunched
)]
struct MyApp();

#[allow(non_snake_case)]
impl MyApp {
    fn OnLaunched(&self, _: &Option<LaunchActivatedEventArgs>) -> Result<()> {
        let window = Window::Current()?;
        window.SetContent(TextBox::new()?)?;
        window.Activate()
    }
}

fn main() -> Result<()> {
    initialize_mta()?;
    Application::Start(ApplicationInitializationCallback::new(|_| {
        MyApp().new()?;
        Ok(())
    }))
}

image

A few loose ends to clean up but a basic Xaml app now works! 😊

All 35 comments

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.

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?

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

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

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

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.

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

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

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

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

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?

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

Just a quick update: I've been working flat-out on this. In the process, I've made many improvements to WinRT support to better handle many aspects of COM and WinRT in Rust. Today I hit a mini-milestone where fully generic authoring support worked for the first time:

fn main() -> Result<()> {
    let t = Thing("hello".to_string());

    let s: windows::foundation::IStringable = t.into();
    println!("{}", s.to_string()?);

    let c: windows::foundation::IClosable = s.cast()?;
    c.close()?;

    Ok(())
}

#[implement(windows::foundation::{IStringable, IClosable})]
struct Thing(String);

impl Thing {
    fn to_string(&self) -> Result<HString> {
        Ok(HString::from(&self.0))
    }

    fn close(&self) -> Result<()> {
        println!("world");
        Ok(())
    }
}

I hope to have a PR out soon.

354 doesn't yet implement everything described in this issue but does allow the example above to work. 😉

Since #[implement] is implemented now (😄), should this issue be resolved?
Edit: I haven't tried it out yet. Just saw it in the new published docs: https://microsoft.github.io/windows-docs-rs/doc/bindings/windows/attr.implement.html

It isn't quite done. It does support basic scenarios involving WinRT interfaces, but lacks support for COM interfaces, and lacks some of the other capabilities necessary for feature complete.

Is it possible to place #[implement] on impl block rather than on struct definition, so it will be more similar to trait implementation?

#[implement]
struct Thing(String);

#[implement(windows::foundation::IStringable)]
impl Thing {
    fn to_string(&self) -> Result<HString> {
        Ok(HString::from(&self.0))
    }
}

#[implement(windows::foundation::IClosable)]
impl Thing {
    fn close(&self) -> Result<()> {
        println!("world");
        Ok(())
    }
}

or maybe even

#[implement]
struct Thing(String);

#[implement]
impl windows::foundation::IStringable for Thing {
    fn to_string(&self) -> Result<HString> {
        Ok(HString::from(&self.0))
    }
}

#[implement]
impl windows::foundation::IClosable for Thing {
    fn close(&self) -> Result<()> {
        println!("world");
        Ok(())
    }
}

I peeked into the implementation and I think the only issue that prevents doing so is the QueryInterface function, but I suppose there could be a way around that using something like https://docs.rs/inventory?

I'm concerned about the implicitly-boxing conversion from impl types to runtime types:

::windows::build!(
    [export]
    pub interface I1 {}
    pub interface I2 {}
    pub class C : I1, I2;
);

#[implement(C)]
struct C(i32);

let c = C(0); // 1) creates a C on the stack
let i1 = I1::from(c); // 2) copies and boxes
let i2 = I2::from(c); // 3) copies and boxes again

This can be confusing to a user familiar with C++/WinRT, who might interpret 1) as creating a heap-allocated C and returning a class-type smart pointer, and 2) 3) as doing nothing more than QI. The user would incorrectly think that c, i1 and i2 all point to the same implementation object.

In actuality, 1) creates a naked C object on the stack, which is an operation that rarely makes sense.

My incomplete personal wishlist of features that might be of your interest:

  1. Support for using self: Pin<&mut Self> and self: Pin<&Self> in impl methods;
  2. Support for using smart pointer types other than Box<_, Global> for allocating COM objects;
  3. Equivalent of C++/WinRT's final_release;
  4. Support for DSTs as impl types (may depend on rust-lang/rust#81513).

I looked into the implementation of the implement! macro again and believed that a wrapper shouldn't be instantiated for each struct, but rather one for all Rust types. This could be achieved by:

  • Define a trait, e.g. RustInspectable which contains the implementation of QueryInterfaces, GetIids and GetRuntimeClassName, as well as a associate type for VTABLE.
  • Have a wrapper, e.g. ComBox with generic <T: RustInspectable>.
  • Redirect all methods calls to the trait.

Thanks for the feedback! I'm just in the middle of overhauling the code gen. Once that's wrapped up I plan to return my focus to this issue.

~@nbdd0121 Looks like this idea can be extended to other COM and WinRT interfaces and classes as well: give every interface and exported class a trait (class traits are empty), so that every type implementing those traits can be used to instantiate a COM or WinRT object with the corresponding interfaces.~

Never mind, I just realized this design would probably necessitate "associated traits" which aren't a thing in Rust.

With weak references now supported (#745 and #718) I'm now working on supporting overridable interfaces for Xaml. I'll probably add generalized support for implementing partial interfaces so that you can, for example, implement Xaml's IApplicationOverride but only provide an implementation of the OnLaunched method. This is going to require a lot better parsing within the implement macro to detect this and stub out the remaining methods to return E_NOTIMPL but I think it will be worth it. Of course, the implement macro will have to know whether it is implementing an overridable interface and in that case forward the call to the base...

#[implement(
    extend Windows::UI::Xaml::Application,
    override OnLaunched,
)]
struct App {
}

impl App {
    fn OnLaunched(&self, _: &LaunchActivatedEventArgs) -> Result<()> {
        let window = Window::Current()?;
        window.SetContent(TextBox::new()?)?;
        window.Activate()
    }
}

Continuing to make good progress here. I can now derive from Xaml's Application class (or any other open composable type). Keep in mind that Rust lacks inheritance. 😉

#[implement(extend Windows::UI::Xaml::Application)]
struct MyApp();

fn main() -> Result<()> {
   let app: Application = MyApp().new()?;

    Ok(())
}

This takes MyApp and wraps it inside a COM/WinRT object that logically extends Application, internally involving COM aggregation, by calling Application's composable constructor to stitch the two objects together.

And this just started working:

#[implement(
    extend Windows::UI::Xaml::Application,
    override OnLaunched
)]
struct MyApp();

#[allow(non_snake_case)]
impl MyApp {
    fn OnLaunched(&self, _: &Option<LaunchActivatedEventArgs>) -> Result<()> {
        let window = Window::Current()?;
        window.SetContent(TextBox::new()?)?;
        window.Activate()
    }
}

fn main() -> Result<()> {
    initialize_mta()?;
    Application::Start(ApplicationInitializationCallback::new(|_| {
        MyApp().new()?;
        Ok(())
    }))
}

image

A few loose ends to clean up but a basic Xaml app now works! 😊

It's slightly beside the point, but Application / Window still requires a UWP context, right?

Yes, this particular example requires an appx. Xaml also works in a standard executable process, it just requires more code to bootstrap, which should also work just fine.

Is the appx needed for deployment and not needed to build the project?

Yes, the appx is only needed for deployment and the appx is only needed if you want to publish a packaged/store app rather than a regular executable that you can xcopy. All of the appx tech is beyond the scope of the Windows crate, which is focused on building the project.

Looking forward to stable support for implementing COM interfaces. I'd especially like to be able to implement the UI Automation provider interfaces in Rust.

With the dependency tracking and usability work done (#896), I'm back working on implementations! Starting with generic interfaces.

@kennykerr How far off do you think you are from supporting non-WinRT COM interfaces in the implement macro? I'm planning to start implementing the UI Automation provider interfaces in a Rust crate soon (within the next few weeks), and I'll write the glue code myself if needed, but it would be great if I didn't have to. In any case, thanks for the update.

Much of #896 was to deal with blocking issues for implementations in general. I hope to get both remaining interface types working soon.

@kennykerr Am I correct that the work so far only allows for implementing WinRT types in Rust, and does not yet have any support for authoring WinRT types for consumption from other languages?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ctaggart picture ctaggart  Â·  4Comments

DJankauskas picture DJankauskas  Â·  3Comments

13r0ck picture 13r0ck  Â·  4Comments

chuckries picture chuckries  Â·  3Comments

rylev picture rylev  Â·  4Comments