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

IMMDevice.Activate fails with IAudioSessionManager2 #1354

Closed
spineki opened this issue Nov 22, 2021 · 26 comments · Fixed by #1805
Closed

IMMDevice.Activate fails with IAudioSessionManager2 #1354

spineki opened this issue Nov 22, 2021 · 26 comments · Fixed by #1805
Labels
enhancement New feature or request

Comments

@spineki
Copy link

spineki commented Nov 22, 2021

Hello, thank you for this amazing library,

I get an error while retrieving back a pointer that should be filled by the IMMDevice::Activate function.

Exception 0xc0000005 encountered at address 0x7ff64e1d65e7: Access violation reading location 0xffffffffffffffff
(exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)

I have got this error on Windows 10 with the following configuration:

  • cargo 1.53.0 (4369396ce 2021-04-27)
  • rustc 1.53.0 (53cb7b09b 2021-06-17)
  • toolchain: stable-x86_64-pc-windows-msvc

Here is a minimal (non!)working example. Code is commented to highlight the problem.

Cargo.toml

[package]
name = "my-package"
version = "0.1.0"
authors = ["spineki"]
edition = "2018"

[dependencies.windows]
version = "0.28.0"
features = [
  "Win32_Foundation",
  "Win32_Media_Audio",
  "Win32_Media_Multimedia",
  "Win32_System_Com",
  "Win32_System_Com_StructuredStorage",
]

Code:

use windows::Win32::{
    Media::Audio::{
        eCapture, eMultimedia, IAudioSessionManager2, IMMDeviceEnumerator, MMDeviceEnumerator,
    },
    System::Com::{
        CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, CLSCTX_INPROC_SERVER,
        COINIT_APARTMENTTHREADED,
    },
};

use windows::core::{Interface, GUID};

fn main() {
    unsafe {
        CoInitializeEx(std::ptr::null(), COINIT_APARTMENTTHREADED).expect("CoInitializeEx Failed");

        // Getting the device enumerator: works
        let imm_device_enumerator: IMMDeviceEnumerator =
            CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER)
                .expect("CoCreateInstance Failed");

        // Getting the IMMDevice of the defaultAudioEndpoint: works
        let endpoint = imm_device_enumerator
            .GetDefaultAudioEndpoint(eCapture, eMultimedia)
            .expect("GetDefaultAudioEnpoint Failed");

        // preparing a pointer that will store the address of the created IAudioSessionManager2
        let mut pinterface: *mut std::ffi::c_void = std::ptr::null_mut();

        // Activating: the target Interface is IAudioSessionManager2: No error!
        endpoint
            .Activate(
                &IAudioSessionManager2::IID,
                CLSCTX_ALL.0,
                std::ptr::null_mut(),
                &mut pinterface as *mut _,
            )
            .expect("Activate Failed");

        // -> Reaching this point, so Activate did not failed

        // Casting back to IAudioSessionManager2: works
        let audio_session_enumerator_ref = (pinterface as *mut IAudioSessionManager2)
            .as_ref()
            .expect("Pointer to ref failed");


        // -------------------- HERE IS THE PROBLEM-------------------- 
        // The call to GetSessionEnumerator fails.
        let audio_session_enumerator = audio_session_enumerator_ref
            .GetSessionEnumerator()
            .expect("GetSessionEnumerator Failed");
        CoUninitialize();
    }
}
@MarijnS95
Copy link
Contributor

MarijnS95 commented Nov 22, 2021

Afaik IAudioSessionManager2 already holds the pointer, so this should be a cast/transmute to IAudioSessionManager2 instead of *mut IAudioSessionManager2.

@spineki
Copy link
Author

spineki commented Nov 22, 2021

Ideed. Thank you so much @MarijnS95 !

For future readers that didn't find an example online like me before,

here is what it looks like

Before

let audio_session_enumerator_ref = (pinterface as *mut IAudioSessionManager2)
    .as_ref()
    .expect("Pointer to ref failed");


// -------------------- HERE IS THE PROBLEM-------------------- 
// The call to GetSessionEnumerator fails.
let audio_session_enumerator = audio_session_enumerator_ref
    .GetSessionEnumerator()
    .expect("GetSessionEnumerator Failed");

After

let audio_session_enumerator_ref =
    std::mem::transmute::<*mut std::ffi::c_void, IAudioSessionManager2>(pinterface);

// The call to GetSessionEnumerator now works!!
let audio_session_enumerator = audio_session_enumerator_ref
    .GetSessionEnumerator()
    .expect("GetSessionEnumerator Failed");

Works like a charm, I can call for example

let count = audio_session_enumerator.GetCount();

println!("count {:?}", count);

@spineki spineki closed this as completed Nov 22, 2021
@MarijnS95
Copy link
Contributor

MarijnS95 commented Nov 22, 2021

I presume this might be a case of fn Activate lacking a _COM_Outptr annotation (and/or the proper uuid, ppv signature) otherwise this should have been turned into a borrow of a generic T: Interface (or a return of that generic T).

Glad you got there in the end with the raw pointer though!

@kennykerr
Copy link
Collaborator

Generally, COM out params will return ownership of a ref-counted object. You should prefer to do something like this:

let mut pinterface: Option<IAudioSessionManager2> = None;

endpoint.Activate(
    &IAudioSessionManager2::IID,
    CLSCTX_ALL.0,
    std::ptr::null_mut(),
    &mut pinterface as *mut _ as _)
    .expect("Activate Failed");

Now the pinterface will own the resulting interface pointer (if any). It is regrettable that the GUID*, void** pair aren't at the back of the parameter list. In such cases, the method is much easier to call because the Windows crate will take care of the binding automatically. I may ask the win32 metadata folks about making this easier.

@kennykerr
Copy link
Collaborator

@MarijnS95 yes, the metadata is missing the attribute as well, but it is also affected by the fact that the GUID parameter isn't the second-to-last, as is conventional.

@MarijnS95
Copy link
Contributor

@kennykerr if the iid parameter were to be annotated, or the _com_outptr contains an index argument of said iid parameter, this is could perhaps work for any signature. Would the win32metadata folks be open to add this (probably through an override / heuristics at first) followed by the SDK headers directly?

@kennykerr
Copy link
Collaborator

I'd prefer to avoid adding new attributes if possible. I think perhaps if the void** parameter had the ComOutPtr attribute (which this method currently lacks) I could then just walk backward and look for a GUID* parameter instead of expecting it to be the second-to-last. I'd have to experiment to see how reliable that is.

@kennykerr
Copy link
Collaborator

Opened microsoft/win32metadata#752 to discuss metadata improvements.

@riverar
Copy link
Collaborator

riverar commented Nov 22, 2021

I think perhaps if the void** parameter had the ComOutPtr attribute (which this method currently lacks) I could then just walk backward and look for a GUID* parameter instead of expecting it to be the second-to-last. I'd have to experiment to see how reliable that is.

Agree, thought of that too when I was building the DIA sample that has a NoRegCoCreate workflow. Am also concerned about false positives, we'd have to take a closer look.

e.g.

HRESULT STDMETHODCALLTYPE NoRegCoCreate(
  const __wchar_t *dllName,
  REFCLSID   rclsid,
  REFIID     riid,
  void     **ppv);

@kennykerr
Copy link
Collaborator

Reopening as #1354 (comment) can now be implemented (hopefully) to enable this API to be transformed.

@kennykerr kennykerr reopened this Jun 3, 2022
@kennykerr kennykerr added the enhancement New feature or request label Jun 3, 2022
@kennykerr
Copy link
Collaborator

kennykerr commented Jun 7, 2022

Another example is D2D1CreateFactory but it lacks the ComOutPtr attribute. microsoft/win32metadata#956

@MarijnS95
Copy link
Contributor

@kennykerr has DXC meanwhile been annotated? I still have a patch sitting that annotates some of the open source repo but that got stuck around nullability vs error cases, iirc.

@kennykerr
Copy link
Collaborator

I'm not sure what you're referring to...

@MarijnS95
Copy link
Contributor

I think it was #1088 (comment).

@kennykerr
Copy link
Collaborator

Currently signatures with a trailing pair of GUID*, void** params that include the appropriate attributes like ComOutPtr will get the query transformation. You can use a tool like ILSpy to quickly confirm the metadata. For example, here's how ILSpy displays the signature for CoCreateInstance:

[DllImport("OLE32", ExactSpelling = true, PreserveSig = false)]
[SupportedOSPlatform("windows5.0")]
public unsafe static extern HRESULT CoCreateInstance([In][Const] Guid* rclsid, [Optional][In] IUnknown pUnkOuter, [In] CLSCTX dwClsContext, [In][Const] Guid* riid, [Out][ComOutPtr] void** ppv);

I'm using this issue to track adding support for more interesting cases where the GUID* and void** parameters are not in this conventional position, functions like D2D1CreateFactory:

[DllImport("d2d1", ExactSpelling = true, PreserveSig = false)]
[SupportedOSPlatform("windows6.1")]
public unsafe static extern HRESULT D2D1CreateFactory([In] D2D1_FACTORY_TYPE factoryType, [In][Const] Guid* riid, [Optional][In][Const] D2D1_FACTORY_OPTIONS* pFactoryOptions, [Out] void** ppIFactory);

Does that cover your case? If not, please open a new issue so I can look into that.

@kennykerr
Copy link
Collaborator

@MarijnS95 #1805 affects one or two DXC APIs - let me know if that resolves your concern.

@MarijnS95
Copy link
Contributor

@kennykerr I don't think the problem is related then. The signatures of those functions were initially detected correctly, but disappeared again once windows-rs started looking for ComOutPtr which the DXC APIs lack... I have a pending patch to add them but that stagnated.

I don't think my questions really fit windows-rs and should otherwise be small enough to sneak in here, no need to create a separate issue (if anything, they're perhaps better suited elsewhere):

  • Simpler signature generation #1088 (comment) / Simpler signature generation #1088 (comment): When a function only ever leaves the outptr NULL if it also returns an error, is that still considered _COM_Outptr_result_maybenull_ or just _COM_Outptr_? This is convention in for example GLib/GStreamer: if the function sets the output error every time it would also return NULL, it is not considered nullable (because you're not supposed to look at function outputs if the function signals an error).
  • What would it take to get fixes to headers in the canonical DXC repo trickled all the way down into the SDK, win32metadata, and finally windows-rs?

@kennykerr
Copy link
Collaborator

Sorry, I'm not very proficient with SAL annotations and I don't know where DXC comes from. You could try asking on the win32metadata repo as those are the same folks who own the Windows SDK.

@damyanp
Copy link
Member

damyanp commented Jun 7, 2022

_COM_Outptr_ parameters must set the output value to null if they return failure. This is alluded to in the documentation where it says:

The returned pointer has COM semantics, which is why it carries an _On_failure_ post-condition that the returned pointer is null.

In sal.h it says:

// Annotations for _Outptr_ parameters which return a pointer to a ref-counted COM object,
// following the COM convention of setting the output to NULL on failure.
// The current implementation is identical to _Outptr_result_nullonfailure_.
// For pointers to types that are not COM objects, _Outptr_result_nullonfailure_ is preferred.

These DXC functions are returning pointers to interfaces that follow COM semantics, and so _COM_Outptr_ can be used here. The definitions in sal.h are also useful to try and figure out what these annotations mean:

#define _COM_Outptr_                        _SAL2_Source_(_COM_Outptr_, (),                      _Outptr_                      _On_failure_(_Deref_post_null_))
#define _COM_Outptr_result_maybenull_       _SAL2_Source_(_COM_Outptr_result_maybenull_, (),     _Outptr_result_maybenull_     _On_failure_(_Deref_post_null_))
#define _COM_Outptr_opt_                    _SAL2_Source_(_COM_Outptr_opt_, (),                  _Outptr_opt_                  _On_failure_(_Deref_post_null_))
#define _COM_Outptr_opt_result_maybenull_   _SAL2_Source_(_COM_Outptr_opt_result_maybenull_, (), _Outptr_opt_result_maybenull_ _On_failure_(_Deref_post_null_))

_COM_Outptr_ must be passed in as a non-null value, and if the function succeeds it will be set to a value and if it fails it will be set to null.

_COM_Outptr_opt_ may be null. If it is non-null then it behaves as for _COM_Outptr_.

The maybenull versions are as above, but the function is allowed to succeed and set the parameter to null.

I suggest you ask on the dxc repo about when they're next planning to update the SDK with the latest DXC changes.

@MarijnS95
Copy link
Contributor

@damyanp Thanks for the links and detailed info! I've now worked this into microsoft/DirectXShaderCompiler#4524 - it's a bit hacky and quick but I found that I couldn't spend much more time on it than that. It seems the particular function I was having issues with has been overridden in win32metadata anyways (I asked this before: will superfluous annotations be removed there if they have found their way into the headers?), yet I actually need the typedef now which doesn't have it yet 😬

@damyanp
Copy link
Member

damyanp commented Jun 20, 2022

I'm happy it was useful.

will superfluous annotations be removed there if they have found their way into the headers?

Is the question here about whether or not we'll clean up the overrides in win32metadata once the headers are updated? If so, then this is something that the win32metadata people can help with.

If I've misunderstood the question, then sorry - could you help me understand it?

@MarijnS95
Copy link
Contributor

Is the question here about whether or not we'll clean up the overrides in win32metadata once the headers are updated?

Yes.

If so, then this is something that the win32metadata people can help with.

I don't need an answer per-se, just curious. Since this can be implemented in the tooling itself I figure it's either already available or will be implemented once (the size of) the overrides gets out of hand :)

@riverar
Copy link
Collaborator

riverar commented Jun 20, 2022

If the headers get updated and ship in the Windows SDK, the changes should just propagate through win32metadata when the targeted SDK is bumped.

@MarijnS95
Copy link
Contributor

@riverar I'm not worried about that, it'll happen at some point presuming my patches ever get merged (just that it is obviously cleaner to not have lingering overrides when that happens). There's a common theme though with my submissions to Microsoft repositories getting ignored, though.

@riverar
Copy link
Collaborator

riverar commented Jun 20, 2022

Is any patch work still needed? (Superfluous annotations?) If we're still missing something in either repo, feel free to holler.

@MarijnS95
Copy link
Contributor

Is any patch work still needed?

microsoft/DirectXShaderCompiler#4524 Would need to get merged, land in win32metadata and finally end up here. And it's useless to me unless windows-rs wants to pick up the arguably-niche #1835.

(Superfluous annotations?)

Nothing that needs to happen, just curious. It seems that https://github.com/microsoft/win32metadata/pull/890/files#diff-12f77aafef2c6ee2fa3a7c437254fc543c6e4246ffee487af8ae24db6d8d5bf0R877-R878 is superfluous if/when microsoft/DirectXShaderCompiler#4524 trickles down into win32metadata, correct?


Regardless, we're straying very far off course for this issue :)

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

Successfully merging a pull request may close this issue.

5 participants