retep998 / winapi-rs

Rust bindings to Windows API
https://crates.io/crates/winapi
Apache License 2.0
1.85k stars 392 forks source link

No documentation on how to use RIDL! macro for COM #354

Open alexchandel opened 7 years ago

alexchandel commented 7 years ago

Other than somewhat confusing usages in https://github.com/retep998/winapi-rs/blob/master/src/shobjidl.rs, there's no documentation on how to use RIDL! on an IDL file or snippet, or how to create a COM server or client from Rust, or how to implement IUnknown for COM types.

For example, how would one go about implementing a COM server like IOPCServer in OPC DA or IOPCServerList in OPC Common? A lot of the implementation seems to happen behind the scenes in MSVC, and it's difficult to see how it carries over to Rust.

retep998 commented 7 years ago

RIDL! is meant to define interfaces so that you can invoke methods on instances of that interface easily. To define an interface using RIDL! should be fairly obvious based on existing usage. To use an interface defined by RIDL!, you acquire a pointer to an IFoo of some sort, and then call methods on it foo.DoSomething(...).

RIDL! does not parse IDL files. Turning IDL files into RIDL! invocations is a manual process (although hopefully someone will write a program to automate it).

Implementing a COM interface yourself is an entirely different matter which I don't provide any convenience macros for yet, partially because it is so complicated.

alexchandel commented 7 years ago

So is RIDL! only useful for the client, or for both?

And using them requires that you already obtained a pointer to the interface somehow? Do you have to initialize the Vtbl as well?

I'm not even sure where to begin implementing a COM server in Rust. You have to do something with CoCreateInstance right?

retep998 commented 7 years ago

Many APIs exist to instantiate COM interfaces for you. DirectX for example has CreateDXGIFactory which is a function that instantiates a IDXGIFactory for you, and then you can call methods on it to instantiate other interfaces. Doing this from Rust is quite easy and is what RIDL! is designed for. You don't have to deal with the vtable manually at all because you get a pointer to an object that is already initialized, and all the interface methods are provided as inherent methods.

If you want to implement an interface yourself for your own type, this is where you have to deal with initializing the vtable yourself, assigning a function for each entry in the vtable, and implementing each of the functions yourself as well. If you want to implement multiple interfaces for a type it gets even worse, because each interface ends up with a different offset relative to the base address of your type, so you have to use thunks to adjust This to the correct pointer.

I don't know anything about COM servers. I only know how the COM ABI works.

alexchandel commented 7 years ago

@retep998 Unfortunately there are no standard APIs to instantiate the COM classes I need. They're normally created with CoCreateInstance/etc.

This seems to cover how to implement a COM interface in a DLL. Implementing it now, and seeing how many times I'm forced to dereference an unsafe pointer from someone else due to the design of COM, I see why so many things make explorer crash.

I took a stab at implementing a single COM interface, IOPCServer, in rust. Look at how much boilerplate garbage there is, and I haven't even added any methods to the interface yet, because the methods all rely on other interfaces.

We need some macros/traits to wrap some of this mess up.

GabrielMajeri commented 6 years ago

I don't know if there is still any interest in this issue, but I needed to implement some COM interfaces with Rust.

I managed to create a procedural macro with syn and quote which generates the required boilerplate. It only works on nightly for now.

The code can be found here. It is meant to be used together with interfaces defined through RIDL!.

CarePackage17 commented 6 years ago

@GabrielMajeri This is pretty awesome! Being able to implement COM interfaces is required to create Windows Runtime apps (which rely on IFrameworkView and IFrameworkViewSource interfaces to set up the window). I'm tempted to try that out.

Edit: just noticed there are no bindings to the WinRT API surface. Got excited a bit too early.

alexchandel commented 6 years ago

Well done @GabrielMajeri. I forgot how horrifically complicated this was. Do your generated implementations perform the many COM checks correctly, like checking whether REFIID is equal to ANY of its interfaces in QueryInterface, including inherited ones? That's a source of many Windows bugs. Does it handle inheritance ?

These are some common COM fails with IUnknown: https://blogs.msdn.microsoft.com/oldnewthing/20040326-00/?p=40033

GabrielMajeri commented 6 years ago

@alexchandel It's very bare bones, and you will have to implement QueryInterface handle checking for REFIIDs manually. You also have to write your own reference counting implementation.

There's nothing keeping you from coming up with your own macros to implement these boilerplates automatically.

It is very unsafe, because it does not check if the method implementation matches the original signature (I didn't find a way to do this easily with proc macros).

It's more of a proof of work for now, I am not actively working on it anymore, but the code is open source.

alexchandel commented 6 years ago

Ah ok. Still saves lots of boilerplate. You might want to link https://blogs.msdn.microsoft.com/oldnewthing/20040326-00/?p=40033 in the README so implementers remember to jump through all the hoops. (I've seen code in the field that breaks the "no secret knock" rule!)

When you declare the interface implementation struct, does it insert any fields other than __vtable?

#[interface(IDXGIFactory)]
struct MyInterface {}

Also, the result of Self::create_vtable() an instance of Vtbl generated by RIDL!? Since that has additional vtables for inherited interfaces, like IUnknown, do you fill in all those as well? Does the box have to be dropped manually in the implementation's Release (might be a good idea to mention that in the README as well)?

GabrielMajeri commented 6 years ago

@alexchandel

When you declare the interface implementation struct, does it insert any fields other than __vtable?

Nope, just puts vtable as the first field in the struct and also ensures the struct is repr(C).

Also, the result of Self::create_vtable() an instance of Vtbl generated by RIDL!?

This function is very simple and #[implementation(IDXGIObject)] simply tries to construct a new IDXGIObjectVtbl struct which should be in scope. This struct should simply be made up of a parent vtbl pointer, and

do you fill in all those as well

By implementing a ComInterface<I> trait for every inherited interface (impl ComInterface<IDXGIFactory>, impl ComInterface<IDXGIObject>) and using Rust's type system, the Self::create_vtable() function will recursively construct the vtable for each and every implemented interface.

Does the box have to be dropped manually in the implementation's Release

All the memory handling stuff must be handled by the implementing crate. The idea is that you never have a raw ComInterface<T>, you always have something like Box<ComInterface<T>>, and the Release function converts &self into a Box and frees it (something like doing delete this in C++).

I will look into updating the README with extra information.