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

Using com-rs on Windows 7 (CoIncrementMTAUsage) #146

Open
fasterthanlime opened this issue Jun 25, 2020 · 6 comments
Open

Using com-rs on Windows 7 (CoIncrementMTAUsage) #146

fasterthanlime opened this issue Jun 25, 2020 · 6 comments

Comments

@fasterthanlime
Copy link

I recently discovered CoIncrementMTAUsage is only available on Windows 8.0 and up itchio/itch#2454

I don't mind finding another solution on my own but my worry is that even then, any application that uses com-rs simply won't load on WIndows 7 because of the missing symbol.

Which means I'd have to maintain a fork that does not rely on CoIncrementMTAUsage - so I was just wondering if it could be behind a cargo feature, perhaps part of the default feature set?

I'm going to test my theory that CoIncrementMTAUsage indeed does not exist on Windows 7 and that a com-rs program as-is will never build with it, I'll update this issue with my findings, but I'd love to hear about someone who knows a lot more about COM than me on this.

@rylev
Copy link
Contributor

rylev commented Jun 25, 2020

I'll let @kennykerr comment on this as he's the COM expert. I believe we would like to support Windows 7 since WinRT-rs also supports it even though the operating system is officially no longer supported by Microsoft.

@kennykerr
Copy link
Collaborator

Yes, the CoIncrementMTAUsage function was introduced in Windows 8. Prior to Windows 8, you need to call CoInitializeEx(0, COINIT_MULTITHREADED) and ensure that the thread that called it remains alive for the duration of any COM activity.

You should be able to simply declare the CoInitializeEx extern in Rust yourself and avoid the com-rs function that calls CoIncrementMTAUsage. That will prevent the linker from adding the symbol to your dependencies and avoid the loader issue on Windows 7 (without having to fork).

In the long run, we will probably drop support for Windows 7 in both C++/WinRT and Rust/WinRT as Windows 7 is no longer supported. The only reason it's still maintained is for Chromium support, but I believe that support is also coming to an end soon.

@fasterthanlime
Copy link
Author

fasterthanlime commented Jun 29, 2020

Thanks to you both for the guidance.

I've come up with a solution that appears to work:

  • Each of my COM interface wrappers hold a ComGuard
    • When built, a ComGuard calls increment(), and it calls decrement() on drop
  • On the first call to either, a background thread is started, which listens for increment/decrement events in a loop
    • If it reaches 1, it calls CoInitializeEx(null_mut(), COINIT_MULTITHREADED)
    • If it reaches 0, it calls CoUninitialize()
  • ComGuard construction can error out, if CoInitializeEx fails, so there's some back and forth messaging going on.

I'd love to get some confirmation that it is sound in principle, if that's possible - here's the full code.


I had one tangentially related question: I see that ComRc is currently !Send. Assuming all threads are in the same MTA (or "free-threaded" as the docs say), is it actually unsafe to send over a ComRc to a different thread? Or would that be okay, but it can't be expressed with Rust's type system, since the COM threading model can be configured at runtime?

I'm asking because I'm exposing a ComRc over FFI (to Go..). The problem is that goroutines typically get scheduled on different threads, so effectively, everything in Go must be Send (but not necessarily Sync). I was thinking of using a Mutex, but it only makes Send types Sync, it does nothing for non-Send types.

edit: Forgot to say that @kennykerr was indeed correct, and that "not calling init_runtime" is enough to not make the resulting executable import CoIncrementMTAUsage at all.

edit 2: just updated the link to runtime.rs, as I had forgotten Co* functions are stdcall, which building on 32-bit gently reminded me of

@kennykerr
Copy link
Collaborator

kennykerr commented Jul 15, 2020

Sorry for the delay, I didn't get a notification about this thread for some reason.

Bumping another atomic ref count whenever a COM interface is created/copied can be costly. That may or not matter but in general that would be prohibitive. The other problem is that you don't necessarily know how many outstanding references there are. One COM object may well spin up other resources that rely on the apartment being alive but those won't necessarily be reflected in your ComGuard's ref count. So I don't believe this solution would be reliable.

Ideally, you can simply use CoIncrementMTAUsage on demand. That's what winrt-rs does. If that's not possible, then I'd recommend a helper function that creates a dedicated thread and calls CoInitializeEx(null_mut(), COINIT_MULTITHREADED) and then put the thread to sleep and forget about it. You could add a Drop trait but then you're back into trying to figure out when to drop it, which is not deterministic - only combase really knows. All of this tidying up is rather theoretical on Windows because combase.dll will almost always be loaded into your process anyway (for any non-trivial app) so all your thread is doing is making that reliable for you and bumping the module ref count on what are sharable memory pages on one of the hottest binaries on Windows so you're truly not saving anything by unloading it. 😉

Regarding Send, you can read up about that here:

microsoft/windows-rs#193

Briefly, if you have a COM interface pointer on an MTA thread, you can freely move/copy that interface pointer to any other MTA thread. That's guaranteed to be safe. It's only when you cross apartment boundaries that you need to be careful.

Oh, and here's how we handle CoIncrementMTAUsage in a way that supports Windows 7.

https://github.com/microsoft/winrt-rs/blob/master/src/runtime.rs

Of course, on Windows 7 we just assume that somebody else will take care of creating an apartment if necessary.

Here's where we create an apartment on demand:

https://github.com/microsoft/winrt-rs/blob/a4835c99e8448caf283cb3aac1cf716a520f192f/src/factory.rs#L20

Happy to answer any other questions you may have.

And one more thing: many "COM" APIs don't actually need an apartment, so you might want to just check whether you even need any of this.

@weltkante
Copy link

And one more thing: many "COM" APIs don't actually need an apartment, so you might want to just check whether you even need any of this.

How do you check this? Is that spelled out in documentation in some way?

@kennykerr
Copy link
Collaborator

The docs might help. Generally, if a COM object is retrieved from an API-specific DLL export like D2D1CreateFactory then you can usually assume that it does not require COM apartments. If a COM object is retrieved via one of the activation functions provided by combase.dll like CoCreateInstance, CoGetClassObject, RoGetActivationFactory, etc. then you can be assured that a COM apartment is required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants