-
Notifications
You must be signed in to change notification settings - Fork 495
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
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:
Today, a Rust project imports types using either the To complement the winrt 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(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 The #[winrt::implements(windows::foundation::Uri)]
pub struct Uri { ... } The implementation will automatically include the The #[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 Finally, the #[winrt::implements(windows::foundation::SomeClass, ISomethingWinRT, ISomethingCom)]
pub struct Uri { ... } In this case, the struct will implement all of the interfaces required by |
I think this all sounds great! I have some questions, hopefully they aren't too in the weeds:
|
Yes, export implies both the production of the winmd file directly from the Rust build as well as the 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 The trouble with your second question is that a procedural macro like |
I'm not sure that this can work since it would need to be guaranteed that the 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). |
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() {}
}
|
@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 |
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 |
@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'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 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. |
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(())
}
|
Hi @kennykerr, is it possible to derive a WinRT type from other custom types for now? The use case is that our |
Little bit of encouragement, I pulled from master and tried Bit of feedback, I had to include |
OMG, this actually works now :) @kennykerr, thank you so much! |
@wravery generally the |
Is there a related Rust tracking issue for that? 👀 |
@kennykerr, I managed to implement all interfaces that I needed except for
would suggest that maybe there is a problem with code generation? |
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:
|
@knopp I think the issue is the signature for |
Yeah... please don't use 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. |
@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 |
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. |
@kennykerr While you're at it, can you please make sure |
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,
}
|
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 (Aside: It might be possible if the generic parameter is required to implement |
Yes, the I do plan to support this eventually. |
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. |
If it weren't for this, I probably never would have used the win32 api |
Authoring support is part of the next major milestone - stay tuned!
The text was updated successfully, but these errors were encountered: