-
Notifications
You must be signed in to change notification settings - Fork 55
Conversation
@@ -35,25 +35,28 @@ impl IUnknownAbi { | |||
quote! { | |||
unsafe extern "stdcall" fn release(this: #this_ptr) -> u32 { | |||
#munge | |||
#class_name::release(&this) | |||
let count = #class_name::release(&munged); | |||
if count == 1 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about this. We're relying on a ref count of 1 meaning that the only handle to the class is the original one which should have been "forgotten" through the use of ManuallyDrop
. If the user forgets to wrap the class in a ManuallyDrop
, and calls query
to get an interface then it will have a ref count of 1 (the handle to the interface requested in query
. When release
is called on that interface the count will be 0. This is unfortunate as it leaks memory, though it is not unsafe.
An alternate implementation would drop the class when the count is 0. However, normally, this would leak memory in the normal case of "forgetting" the class when created by a class factory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually also possible to have a dangling pointers if this function is called with a class that has not been "forgotten":
If I return an interface from IClassFactory::create_instance that has been created from a class struct that was then dropped, it's ref count will be 1 (just the handle to interface instead of 2 for the handle and the original class object). If I then make another handle, and it gets released through the call to release ABI function, it will free the memory even though there is still a handle to an interface out there.
This requires improperly implementing an unsafe function though so I don't think it's not an obviously wrong way to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usual way to handle this is for the "make" function to allocate the object and return the only reference to the object as a COM interface. That COM interface holds that first reference count.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is what we're currently doing, though the user can allocate on their own (i.e., there's nothing stopping them other than us documenting that they shouldn't). Our "make" function is called alloc
and it does just this. I'm looking for a way to prevent users from holding on to COM classes at all since they should use alloc
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so in C++/WinRT we have a virtual function called use_make_function_to_create_this_object that solves this problem. Perhaps in Rust we could have the extra fields needed for COM (e.g. vfptrs and ref count) such that they are not Default
so you cannot simply construct the object yourself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I'm currently thinking how we want to do this. The code is being generated by the macro so it's currently as if the user wrote the code themselves (i.e., everything the macro produces, the user must also be able to write themselves). I'm thinking about how we can give the macro the ability to create the class but not give this ability to the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a change that should help.
It's still technically possible inside of the module where the class is declared to instantiate the class on the stack, but this has be done by hand and is not trivial (it's about 225 lines of total code in the basic server example). The class is not Default so there's no way to instantiate that way (though you can call MyClass::allocate_default()
to get a proper heap allocated version where all the user fields are defaulted). Outside of the current module it's not possible at all to stack allocate the class.
Obviously we need docs to clarify how this all should work, but it's now highly unlikely that the user will do the wrong thing.
let factory = get_class_object::<IClassFactory>(&CLSID_CAT_CLASS) | ||
.unwrap_or_else(|hr| panic!("Failed to get cat class object 0x{:x}", hr)); | ||
println!("Got cat class object"); | ||
|
||
// Get an instance of a `BritishShortHairCat` as the `IUnknown` interface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this is just an example and not a requirement. 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean you hope it's not a requirement to get the IUnknown interface? It's not as get_instance
is generic over the interface you want to get the class as.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, the example just made it look like I needed to retrieve the interface and then later query for the desired interface.
|
||
// Get a `BritishShortHairCat` class factory | ||
let factory = get_class_object::<IClassFactory>(&CLSID_CAT_CLASS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there also an equivalent of CoCreateInstance(Ex)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a call to CoGetClassObject. We use create_instance
below which directly maps to CoCreateInstance
.
let factory = get_class_object::<IClassFactory>(&CLSID_CAT_CLASS) | ||
.unwrap_or_else(|hr| panic!("Failed to get cat class object 0x{:x}", hr)); | ||
println!("Got cat class object"); | ||
|
||
// Get an instance of a `BritishShortHairCat` as the `IUnknown` interface | ||
let unknown = factory | ||
.get_instance::<IUnknown>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is CreateInstance
renamed to get_instance
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because there is no method overloading. I decided to rename the safe functions since they have different signatures and thus calling the safe version create_instance
might be confusing. I'm starting to think that for the raw win32 COM API calls we should use the CamelCase names and the safe versions can use the snake_case convention. This might be a better way to distinguish the two. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I don't renaming is such a good idea. It implies they do something else.
let unknown = factory | ||
.get_instance::<IUnknown>() | ||
.expect("Failed to get IUnknown"); | ||
println!("Got IUnknown"); | ||
|
||
// Now get a handle to the `IAnimal` interface | ||
let animal = unknown | ||
.get_interface::<IAnimal>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is QueryInterface
renamed to get_interface
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. This is the safe wrapper around query_interface
. We could rename the unsafe version QueryInterface
and the safe version could then be query_interface
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that may be better.
// User field input as parameters to the allocate function. | ||
fn gen_allocate_user_fields(class: &Class) -> TokenStream { | ||
let field_idents = class.fields.iter().map(|f| &f.ident); | ||
/// Function used to instantiate a default version COM class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this comment is trying to tell me. Version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops. This is a function used to create an instance of the class where all the user fields are default. We don't want to provide a normal Default
implementation because that would allow the user to stack allocate the class which we don't want.
macros/support/src/class/class.rs
Outdated
let default_constructor = super::class_constructor::generate_default(self); | ||
let unsafe_constructor = super::class_constructor::generate_unsafe(self); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to derive(Debug)
a while ago not all user-specified members may have Default
implemented; this currently makes it impossible to create a COM class with such members.
Would it be possible to emit these only when the class
definition is annotated with #[derive(Default)]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't want to allow annotating the class with #[derive(Default)]
as either this creates a Default::default
implementation which would allow for stack allocation (which we don't want to allow) or it doesn't which is confusing. I think we'll need some other attribute to indicate that your type is defaultable. This is needed for use with CoCreateInstance
as the implementation of that function creates an instance of the class. I'll need to think more about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MarijnS95 I added the ability to add a #[no_class_factory]
attribute to your class. This removes the class factory generation (meaning you can't use CoCreateInstance
) along with any need for your data members to be Default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll probably chain that into Linux support at some point as CoCreateInstance
isn't available there anyway.
generate_unsafe
is still there emitting an almost identical constructor to generate_default
including forced defaults for members. I guess that should be behind has_class_factory
as well, which is what you need this for?
Besides allocate
has to return some type that implements the Interface
trait, which is not the case for the class
itself. This means it's currently impossible to receive Self
(or a Pin<Box<Self>>
) to interact with these members after constructing. Is that intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh whoops. Forgot to not return early from generate_unsafe
if has_class_factory
is false. Will change that.
Yes this is on purpose but that's usually what you want. Can you explain your use case? You want to be able to call a regular method on the class object without going through an interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think putting deallocation in the
Class
trait would complicate things. You'd then have to make sure that all fields are still valid to keep around after deallocation. So for instance if your class had aVec
as one of its fields, you'd have to wrap it in aManuallyDrop
to get things to work. Right now, the class object only actually gets dropped when it should be deallocated.
I see, ClassAllocation::drop
and the release
function can't easily be generalized as the drop takes by mutable reference while release
intentionally takes the ClassAllocation
by value to implicitly call drop()
. No way we should go back to wrapping all members in ManuallyDrop
and that kind of shenanigans 😁
We could switch to atomics for the ref count, but I believe more work would be needed to ensure thread safety. Write now we assume that the class is not accessed from different threads. I'm not sure how we'd handle user defined fields that are not thread safe.
While tidying up the implementation (getting rid of get
+set
pairs) this might give a false sense of thread-safe security, your call.
I'm not sure if Self: Pin<Box> is entirely necessary though it is definitely more correct than &self since Self in that case is the class not the ClassAllocation. I'd prefer to keep it.
I wonder if query
/query_interface
/add_ref
should worry about such a thing. Thanks to Deref
(if it were to return &Self
) it would anyway be lifetime-bound to a particular ClassAllocation
instance. Removing it makes the thing segfault, so let's leave it in 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a debug impl for the ClassAllocation.
@rylev the idea was to forward the formatting to inner
in case it supports debugging (as per the code suggestion). Showing a raw pointer to the user seems rather pointless IMO, except for developers on com-rs itself.
The error is pretty helpful too, in case someone forgot to implement debug on their com class but try to dbg
it:
error[E0277]: `wrapper::DxcIncludeHandlerWrapper` doesn't implement `std::fmt::Debug`
--> src/wrapper.rs:270:9
|
270 | dbg!(&cls);
| ^^^^^^^^^^^ `wrapper::DxcIncludeHandlerWrapper` cannot be formatted using `{:?}`
|
= help: the trait `std::fmt::Debug` is not implemented for `wrapper::DxcIncludeHandlerWrapper`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
= note: required because of the requirements on the impl of `std::fmt::Debug` for `com::production::ClassAllocation<wrapper::DxcIncludeHandlerWrapper>`
Unfortunately + ?std::fmt::Debug
isn't a thing, otherwise we could have both. For now I'd suggest/request to implement Debug
only when T
implements it too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thar's fair. The implementation I included was actually very helpful for debugging the segfault issue we were seeing, but I can see how that's potentially not helpful in the general case where COM-rs is not buggy 😉.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rylev I guess a ClassAllocation@0x<address> { <your struct> }
?
But then we don't want that to segfault before the address has been printed 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we keep it as is and add an AsRef
impl for ClassAllocation
so it's very easy to get a &T
which can be debug printed when T: Debug
. Either way this is now being tracked: #169
@kennykerr - I think this PR is ready to go. You had some concerns about naming, but that's not really a part of this PR (as that exists in master). Shall we merge this and open an issue for your concerns about names? |
@kennykerr @MarijnS95 I'm going to merge this and add issues for the things we mentioned during the review:
|
@rylev Yeah it seems totally ready from my side, thanks for working through the remaining issues 🙂! |
Fixes #161
This (hopefully) fixes the issues in #161 which are:
&self
. However, the IUnknown methods assume that self is heap allocated and won't move. We model this by taking self asself: &Pin<Box<Self>>
. This makes it not possible to call IUnknown methods on a stack allocated object. If users must assume that their code is heap allocated they can declare their methods with the same signature as the IUnknown methods but if the assumption is not needed, they can just use&self
.query
. This may only be used when the type is heap allocated and pinned.alloc
associated function which heap allocates the class, pins it, and queries the class for a given interface.add_ref
andrelease
respectively. On drop, the ref count is also decremented. When the count is 0, the class's contents are dropped. This should only happen inside of therelease
thunk or if the class is manually allocated by the user and never shared.