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

MFTEnumEx memory management confusion #1685

Closed
CarePackage17 opened this issue Apr 12, 2022 · 9 comments
Closed

MFTEnumEx memory management confusion #1685

CarePackage17 opened this issue Apr 12, 2022 · 9 comments
Labels
question Further information is requested

Comments

@CarePackage17
Copy link

I'm having trouble with the MFTEnumEx function. It's documentation says that I'm supposed to call Release on each of the returned IMFActivate pointers, which as far as I could tell is a Drop impl on ::windows::core::IUnknown (there are no methods to call explicitly like in C++).
I was a bit confused getting this to compile at all, in particular the maybe_activate line; there's some stuff going on with references that I don't fully understand, but just dereferencing the pointer without borowing resulted in a move which didn't compile because Option<IMFActivate> does not implement Copy.

As you can see I'm calling drop on whatever I get out of the array, though I suspect this is incorrect. If I place a breakpoint on IUnknown's Drop function, it is not hit. So I guess I'm freeing the reference (a no-op) instead of the IMFActivate object that I'm supposed to free.

Can anyone explain how to use this function correctly?

use windows::Win32::{Media::MediaFoundation::{
    IMFActivate, MFShutdown, MFStartup, MFTEnumEx, MFSTARTUP_NOSOCKET, MFT_CATEGORY_VIDEO_ENCODER,
    MFT_ENUM_FLAG_HARDWARE, MF_API_VERSION, MF_SDK_VERSION,
}, System::Com::CoTaskMemFree};

fn main() -> windows::core::Result<()> {
    let mf_version = (MF_SDK_VERSION << 16) | MF_API_VERSION;
    unsafe {
        let res = MFStartup(mf_version, MFSTARTUP_NOSOCKET);

        if res.is_ok() {
            println!("Media Foundation initialized!");

            let mut tmp: Option<IMFActivate> = None;
            let mut activate_ptrs = &mut tmp as *mut Option<IMFActivate>;
            let mut activate_ptr_count = 0u32;

            let res = MFTEnumEx(
                MFT_CATEGORY_VIDEO_ENCODER,
                MFT_ENUM_FLAG_HARDWARE.0.try_into().unwrap(),
                std::ptr::null(),
                std::ptr::null(),
                &mut activate_ptrs,
                &mut activate_ptr_count,
            );
            if res.is_ok() {
                println!("MFTEnumEx succeeded, {} MFTs returned!", activate_ptr_count);

                //call release on every IMFActivate pointer
                for i in 0..activate_ptr_count {
                    let maybe_activate = &mut *activate_ptrs.add(i as usize);
                    if let Some(activate) = maybe_activate {
                        drop(activate);
                    }
                }

                CoTaskMemFree(activate_ptrs as *const std::ffi::c_void);
                println!("Freed activate_ptrs");
            }

            let res = MFShutdown();
            assert_eq!(res.is_ok(), true);
        } else {
            println!("{:?}", res.err());
        }
    }

    Ok(())
}
@robmikh
Copy link
Member

robmikh commented Apr 12, 2022

I need to update to the newest version of the windows crate, but here's how I call it in displayrecorder:

https://github.com/robmikh/displayrecorder/blob/9e9617bea73847c8ff801731c7bd6ee32edf919b/src/media.rs#L20-L57

@CarePackage17
Copy link
Author

I need to update to the newest version of the windows crate, but here's how I call it in displayrecorder:

https://github.com/robmikh/displayrecorder/blob/9e9617bea73847c8ff801731c7bd6ee32edf919b/src/media.rs#L20-L57

This is helpful, thank you!

I didn't think about using transmute because I've mostly done safe Rust so far (and because of the big scary warnings in the doc); is this the recommended way of dealing with out pointers to COM interfaces?

@robmikh
Copy link
Member

robmikh commented Apr 12, 2022

No problem!

In this case, where you're getting a list back that's allocated by the function, transmute would be what I recommend. There shouldn't be null entries in that list and you know how long it's supposed to be, which makes it great for a slice.

Generally, sticking with the Option and unwrapping it is what I go with for COM out pointers (e.g. ID3D11Device::GetImmediateContext).

@kennykerr kennykerr added the question Further information is requested label Apr 12, 2022
@CarePackage17
Copy link
Author

CarePackage17 commented Apr 13, 2022

So I have copied the code from your repo (without lines 47-48), but something seems off still; when debugging I never end up in IUnknown's Drop impl because iterating over the slice gives me shared references to IMFActivate, which don't seem to actually call Release on the underlying COM object.

I've tried with VSCode and WinDbg, and with the latter I expected to end up in some Windows DLL where the COM object's Release method is implemented, which did not happen. The stack frame's name in WinDbg is mfrust!core::mem::drop<ref$<windows::Windows::Win32::Media::MediaFoundation::IMFActivate>> which implies I'm dropping the ref, not the object behind it.

Code looks like this now:

let mut mfactivate_list = std::ptr::null_mut();
let mut num_mfactivate = 0u32;
            let res = MFTEnumEx(
                MFT_CATEGORY_VIDEO_ENCODER,
                MFT_ENUM_FLAG_HARDWARE.0.try_into().unwrap(),
                std::ptr::null(),
                std::ptr::null(),
                &mut mfactivate_list,
                &mut num_mfactivate,
            );

if res.is_ok() {
            println!("MFTEnumEx succeeded, {} MFTs returned!", num_mfactivate);

            //call release on every IMFActivate pointer
            let mfactivate_list: *mut IMFActivate = std::mem::transmute(mfactivate_list);
            let mfactivate_slice =
                std::slice::from_raw_parts(mfactivate_list, num_mfactivate as usize);
            for mfactivate in mfactivate_slice {
                    // We need to release each item
                    std::mem::drop(mfactivate);
            }

            CoTaskMemFree(mfactivate_list as *const std::ffi::c_void);
            println!("Freed activate ptrs");
}

Is this a bug or am I misunderstanding how Drop is supposed to work?

@robmikh
Copy link
Member

robmikh commented Apr 13, 2022

No, it looks like you've got it right. Thanks for catching this.

This is the only way I could think of to fix it:

unsafe {
    // If we have more than one IMFActivate in the list,
    // we can transmute it out of the Option<_>
    let mfactivate_list: *mut IMFActivate = std::mem::transmute(mfactivate_list);
    let mfactivate_slice =
        std::slice::from_raw_parts(mfactivate_list, num_mfactivate as usize);
    for mfactivate in mfactivate_slice {
        let transform_source = mfactivate.clone();
        // This is a dirty trick we play so that we can
        // release the underlying IMFActivate despite having
        // a shared reference.
        let temp: windows::core::IUnknown = std::mem::transmute_copy(&transform_source.0);
        transform_sources.push(transform_source);
        // We need to release each item
        std::mem::drop(temp)
    }
    // Free the memory that was allocated for the list
    CoTaskMemFree(mfactivate_list as *const _);
}

If you create a Vec instead of a slice, it seems to drop the items but then you can't pass the list to CoTaskMemFree. @kennykerr, do you know of a better way of handling this case?

@kennykerr
Copy link
Collaborator

Yeah, this is a difficult API to call properly. It probably needs a little help.

use windows::{core::*, Win32::Media::MediaFoundation::*, Win32::System::Com::*};

fn main() -> Result<()> {
    unsafe {
        let mut data = std::ptr::null_mut();
        let mut len = 0;
        MFTEnumEx(
            MFT_CATEGORY_VIDEO_ENCODER,
            MFT_ENUM_FLAG_HARDWARE.0 as _,
            std::ptr::null(),
            std::ptr::null(),
            &mut data,
            &mut len,
        )?;
        let array = ComArray::<IMFActivate>::from_raw(data as _, len);
        for i in array.as_slice() {
            println!("{}", i.GetCount()?);
        }
        Ok(())
    }
}

struct ComArray<T: Interface> {
    data: *mut T,
    len: u32,
}

impl<T: Interface> ComArray<T> {
    pub unsafe fn from_raw(data: *mut T, len: u32) -> Self {
        Self { data, len }
    }
    pub fn is_empty(&self) -> bool {
        self.len == 0
    }
    pub fn as_slice(&self) -> &[T] {
        if self.is_empty() {
            return &[];
        }

        unsafe { std::slice::from_raw_parts(self.data, self.len as usize) }
    }
}

impl<T: Interface> Drop for ComArray<T> {
    fn drop(&mut self) {
        if self.is_empty() {
            return;
        }

        unsafe {
            std::ptr::drop_in_place(std::slice::from_raw_parts_mut(self.data, self.len as usize));
            CoTaskMemFree(self.data as _);
        }
    }
}

A bit more work up front but then you know it's taken care of.

@kennykerr
Copy link
Collaborator

There's also windows::core::Array which is also backed by the same allocator. We could potentially generalize Array<T> to cover such APIs as well.

@CarePackage17
Copy link
Author

Thanks a lot for the help. :)

@kennykerr
Copy link
Collaborator

Just a quick update. With #2362 you can use the windows::core::Array type to greatly simplify this. For example:

use windows::{core::*, Win32::Media::MediaFoundation::*};

fn main() -> Result<()> {
    unsafe {
        let mut data = std::ptr::null_mut();
        let mut len = 0;
        MFTEnumEx(
            MFT_CATEGORY_VIDEO_ENCODER,
            MFT_ENUM_FLAG_HARDWARE,
            None,
            None,
            &mut data,
            &mut len,
        )?;
        let array = Array::<IMFActivate>::from_raw_parts(data as _, len);
        for i in array.as_slice() {
            println!("{}", i.as_ref().unwrap().GetCount()?);
        }
        Ok(())
    }
}

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

No branches or pull requests

3 participants