Работа с COM-объектами .NET Framework

Пытаюсь использовать функцию GetApplicationVolume из примера https://gist.github.com/sverrirs/d099b34b7f72bb4fb386 вот так:

public static float? GetApplicationVolume(int pid)
{
    ISimpleAudioVolume volume = GetVolumeObject(pid);
    if (volume == null)
        return null;

    float level;
    volume.GetMasterVolume(out level);
    Marshal.ReleaseComObject(volume);
    return level * 100;
}

private static ISimpleAudioVolume GetVolumeObject(int pid)
{
    IMMDeviceEnumerator deviceEnumerator = null;
    IAudioSessionEnumerator sessionEnumerator = null;
    IAudioSessionManager2 mgr = null;
    IMMDevice speakers = null;
    try
    {
        // get the speakers (1st render + multimedia) device
        deviceEnumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator());
        deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out speakers);

        // activate the session manager. we need the enumerator
        Guid IID_IAudioSessionManager2 = typeof(IAudioSessionManager2).GUID;
        object o;
        speakers.Activate(ref IID_IAudioSessionManager2, 0, IntPtr.Zero, out o);
        mgr = (IAudioSessionManager2)o;

        // enumerate sessions for on this device
        mgr.GetSessionEnumerator(out sessionEnumerator);
        int count;
        sessionEnumerator.GetCount(out count);

        // search for an audio session with the required process-id
        ISimpleAudioVolume volumeControl = null;
        for (int i = 0; i < count; ++i)
        {
            IAudioSessionControl2 ctl = null;
            try
            {
                sessionEnumerator.GetSession(i, out ctl);

                // NOTE: we could also use the app name from ctl.GetDisplayName()
                int cpid;
                ctl.GetProcessId(out cpid);

                if (cpid == pid)
                {
                    volumeControl = ctl as ISimpleAudioVolume;
                    break;
                }
            }
            finally
            {
                if (ctl != null) Marshal.ReleaseComObject(ctl);
            }
        }

        return volumeControl;
    }
    finally
    {
        if (sessionEnumerator != null) Marshal.ReleaseComObject(sessionEnumerator);
        if (mgr != null) Marshal.ReleaseComObject(mgr);
        if (speakers != null) Marshal.ReleaseComObject(speakers);
        if (deviceEnumerator != null) Marshal.ReleaseComObject(deviceEnumerator);
    }
}
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(AudioManager.GetApplicationVolume(5992).ToString());
    }
}

и получаю "Необработанное исключение: System.Runtime.InteropServices.InvalidComObjectException: Объект COM, который был отделен от своего базового RCW, использоваться не может.

При гуглении залез в какие дебри про потоки и совсем запутался. Помогите! C C# и .NET до этого не работал, так что не кидайтесь тапками)


Ответы (1 шт):

Автор решения: aepot

Вот смотрите, в C# есть IDisposable объекты. Смысл этого интерфейса в том чтобы помочь разработчику правильно очищать неуправляемые ресурсы даже при возникновении исключений в управляемом коде. Либо отдать контроль своими неуправляемыми ресурсами сборщику мусора, но это уже другая история про финализаторы.

Для того, чтобы гарантированно вызвать какой-то код даже при возникновении исключения, используется конструкция try-finally или конкретно для IDisposable есть ее упрощенный синтаксис using. Почитать больше можно здесь: Использование объектов, реализующих IDisposable.

Так к чему я всё это. COM-объекты, это по сути вызов неуправляемого кода, например написанного на C++, но сделанного так, чтобы с ним можно было взаимодействовать через специальное API операционной системы под названием COM (Component Object Model). Так вот, если у реализующего IDisposable класса надо обязательно вызывать Dispose(), то у COM-объекта надо обязательно вызывать Marshal.ReleaseComObject(), при чём используя ту же самую конструкцию try-finally, чтобы не быть причиной утечек памяти в случае, если возниколо исключение в управляемом коде и выполнение кода дальше не пошло.

try
{
    var obj = getComCobject();
    // работаем с COM-объектом
}
finally
{
    Marshal.ReleaseComObject(obj);
}

И разработчик этого класса AudioManager очень строго следует этим принципам, и он перестарался.

Дело в том, что строки кода в методе GetVolumeObject выполняются (при попадании в нужный PID) в следующем порядке

// найден нужный COM-объект
if (cpid == pid) // true
{
    volumeControl = ctl as ISimpleAudioVolume;
    break;
}

// возврат результата
return volumeControl;

// и следом срабатывает
finally
{
    if (ctl != null) Marshal.ReleaseComObject(ctl);
}

И при возврате в метод, вы пытаетесь обратиться к COM-объекту, для которого получается, что ранее уже был вызван Marshal.ReleaseComObject.

volume.GetMasterVolume(out float level);

Говоря дотнетовским языком, пытаетесь воспользоваться уже ранее уничтоженным объектом.

Что же делать? Самый простой способ - обмануть конструкцию блоке finally

finally
{
    if (ctl != null) Marshal.ReleaseComObject(ctl);
}

Вот так

if (cpid == pid)
{
    volumeControl = ctl as ISimpleAudioVolume;
    ctl = null; // эту строчку нужно добавить в класс
    break;
}

В этом случае volumeControl вернется из метода GetVolumeObject в метод GetApplicationVolume в рабочем состоянии.

Я проверил подставив PID одного из своих процессов, для наглядности уменьшил немного громкость приложению, с которого её считывал.

Console.WriteLine(AudioManager.GetApplicationVolume(24240) ?? 0);

Вывод в консоль:

93,61702

Ну и вот это тоже работает

Console.WriteLine(AudioManager.GetMasterVolume());
47
→ Ссылка