もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
以前の記事で、マイクスピーカーを扱うC++を書いたが、メモリリークしまくりだった。
https://tera1707.com/entry/2023/10/10/224852
こういうコードだった。
※リークしている様子がわかりやすいように、処理を無限ループの中に入れた。
#include <windows.h> #include <mmdeviceapi.h> #include <functiondiscoverykeys.h> #include <endpointvolume.h> #include <string> int main() { // COMの初期化(COMのお作法) HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); while (TRUE) { IMMDeviceEnumerator* pEnum = NULL; IMMDeviceCollection* pCollection = NULL; UINT deviceCount = 0; // COMからMMDeviceEnumeratorを取ってくる hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)); // オーディオエンドポイントの列挙を実行 hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection); // とれた数を数える hr = pCollection->GetCount(&deviceCount); for (int i = 0; i < deviceCount; i++) { // デバイスの情報を取る IMMDevice* pEndpoint = NULL; hr = pCollection->Item(i, &pEndpoint); // デバイスのプロパティを取る IPropertyStore* pProperties; hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties); // 取ったプロパティから、値を指定して取得する PROPVARIANT vName; PropVariantInit(&vName); hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName); // エンドポイントの情報を取る IAudioEndpointVolume* pAudioEndVol = NULL; hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol); // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい) float getVolume = 0.0f; hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume); // 音量を取得 float setVolume = 0.25f; hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL); // 音量を設定 std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n"; OutputDebugString(outString.c_str()); } } // COMの後処理(COMのお作法) CoUninitialize(); return 0; }
メモリリークの様子
→動かすと、ハンドルとWorkingSet(メモリ使用量)がガンガン増えていく...
これでは使い物にならないので、ちゃんと使用済みのCOMのポインタを開放する処理を入れたい。
Releaseを入れたコード
下記にあるSafeReleaseを使って、解放を行う。
#include <windows.h> #include <mmdeviceapi.h> #include <functiondiscoverykeys.h> #include <endpointvolume.h> #include <string> // SafeRelease // https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/com-coding-practices#the-saferelease-pattern // エラー処理 // https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com template <class T> void SafeRelease(T** ppT) { if (*ppT) { (*ppT)->Release(); *ppT = NULL; } } int main() { // COMの初期化(COMのお作法) HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); while (TRUE) { IMMDeviceEnumerator* pEnum = NULL; IMMDeviceCollection* pCollection = NULL; UINT deviceCount = 0; // COMからMMDeviceEnumeratorを取ってくる hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)); if (hr != S_OK) { return -1; } // オーディオエンドポイントの列挙を実行 hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection); if (hr != S_OK) { SafeRelease(&pEnum); return -1; } // とれた数を数える hr = pCollection->GetCount(&deviceCount); if (hr != S_OK) { SafeRelease(&pEnum); SafeRelease(&pCollection); return -1; } for (int i = 0; i < deviceCount; i++) { // デバイスの情報を取る IMMDevice* pEndpoint = NULL; hr = pCollection->Item(i, &pEndpoint); if (hr != S_OK) continue; // デバイスのプロパティを取る IPropertyStore* pProperties; hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties); if (hr != S_OK) { SafeRelease(&pEndpoint); continue; } // 取ったプロパティから、値を指定して取得する PROPVARIANT vName; PropVariantInit(&vName); hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName); if (hr != S_OK) { SafeRelease(&pEndpoint); SafeRelease(&pProperties); PropVariantClear(&vName); continue; } // エンドポイントの情報を取る IAudioEndpointVolume* pAudioEndVol = NULL; hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol); if (hr != S_OK) { SafeRelease(&pEndpoint); SafeRelease(&pProperties); PropVariantClear(&vName); continue; } // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい) float getVolume = 0.0f; hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume); // 音量を取得 if (hr != S_OK) { SafeRelease(&pEndpoint); SafeRelease(&pProperties); PropVariantClear(&vName); continue; } float setVolume = 0.25f; hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL); // 音量を設定 if (hr != S_OK) { SafeRelease(&pEndpoint); SafeRelease(&pProperties); PropVariantClear(&vName); continue; } std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n"; OutputDebugString(outString.c_str()); // 後処理 SafeRelease(&pEndpoint); SafeRelease(&pProperties); PropVariantClear(&vName); SafeRelease(&pAudioEndVol); } // 後処理 SafeRelease(&pCollection); SafeRelease(&pEnum); } // COMの後処理(COMのお作法) CoUninitialize(); return 0; }
解放を入れた後のハンドルとメモリ使用量
これでうまく解放できているっぽいが、コードがSafeRelease()
だらけになる....
CComPtrを使う その①(エラー処理無し)
CComPtr<>
という、自動でCOMのポインタを解放してくれる便利なものがあるらしい。
それでやってみる。
#include <windows.h> #include <string> #include <mmdeviceapi.h> #include <functiondiscoverykeys.h> #include <endpointvolume.h> #include "atlbase.h" // CComPtr int main() { // COMの初期化(COMのお作法) HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); while (TRUE) { CComPtr<IMMDeviceEnumerator> pEnum = NULL; CComPtr<IMMDeviceCollection> pCollection = NULL; UINT deviceCount = 0; // COMからMMDeviceEnumeratorを取ってくる hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)); // オーディオエンドポイントの列挙を実行 hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection); // とれた数を数える hr = pCollection->GetCount(&deviceCount); for (int i = 0; i < deviceCount; i++) { // デバイスの情報を取る CComPtr<IMMDevice> pEndpoint = NULL; hr = pCollection->Item(i, &pEndpoint); // デバイスのプロパティを取る CComPtr<IPropertyStore> pProperties; hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties); // 取ったプロパティから、値を指定して取得する PROPVARIANT vName; PropVariantInit(&vName); hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName); // エンドポイントの情報を取る CComPtr<IAudioEndpointVolume> pAudioEndVol = NULL; hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol); // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい) float getVolume = 0.0f; hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume); // 音量を取得 float setVolume = 0.25f; hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL); // 音量を設定 std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n"; PropVariantClear(&vName); OutputDebugString(outString.c_str()); } // 後処理はかってにやってくれる! } // COMの後処理(COMのお作法) CoUninitialize(); return 0; }
これでも、メモリはちゃんと解放されてくれてるようで、メモリ使用量は増えなかった。
が、👆のコードだとエラー発生時に、その後の処理を継続してしまうのでエラー処理を入れたい。
CComPtrを使う その②(エラー処理あり:Cascading ifs パターン)
↓のやり方で、エラー処理を記述してみた。
https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com#cascading-ifs
それがこれ。
#include <windows.h> #include <string> #include <mmdeviceapi.h> #include <functiondiscoverykeys.h> #include <endpointvolume.h> #include <comdef.h> #include "atlbase.h" // CComPtr int main() { // COMの初期化(COMのお作法) HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); while (TRUE) { CComPtr<IMMDeviceEnumerator> pEnum = NULL; CComPtr<IMMDeviceCollection> pCollection = NULL; UINT deviceCount = 0; // COMからMMDeviceEnumeratorを取ってくる hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)); if (SUCCEEDED(hr)) { // オーディオエンドポイントの列挙を実行 hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection); } if (SUCCEEDED(hr)) { // とれた数を数える hr = pCollection->GetCount(&deviceCount); } for (int i = 0; i < deviceCount; i++) { // デバイスの情報を取る CComPtr<IMMDevice> pEndpoint = NULL; if (SUCCEEDED(hr)) { // デバイスの情報を取る hr = pCollection->Item(i, &pEndpoint); } CComPtr<IPropertyStore> pProperties; if (SUCCEEDED(hr)) { // デバイスのプロパティを取る hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties); } PROPVARIANT vName; if (SUCCEEDED(hr)) { // 取ったプロパティから、値を指定して取得する PropVariantInit(&vName); hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName); } CComPtr<IAudioEndpointVolume> pAudioEndVol = NULL; if (SUCCEEDED(hr)) { // エンドポイントの情報を取る hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol); } // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい) float getVolume = 0.0f; if (SUCCEEDED(hr)) { // 音量を取得 hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume); } float setVolume = 0.25f; if (SUCCEEDED(hr)) { // 音量を設定 hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL); } if (SUCCEEDED(hr)) { // 表示 std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n"; PropVariantClear(&vName); OutputDebugString(outString.c_str()); } } } // COMの後処理(COMのお作法) CoUninitialize(); return 0; }
これもメモリはちゃんと解放はされてるが、なんかコードの流れは分かる..?のだが、if (SUCCEEDED(hr))
だらけになるのと、今後の変更で途中になにか処理を(間違えて)入れてしまったらいやだな、、、というのが気になる。。。
CComPtrを使う その③(エラー処理あり:Throw on Fail パターン)
↓のやり方で、エラー処理を記述してみた。
https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com#throw-on-fail
それがこれ。
#include <windows.h> #include <string> #include <mmdeviceapi.h> #include <functiondiscoverykeys.h> #include <endpointvolume.h> #include <comdef.h> #include "atlbase.h" // CComPtr inline void throw_if_fail(HRESULT hr) { if (FAILED(hr)) { throw _com_error(hr); } } int main() { // COMの初期化(COMのお作法) HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); while (TRUE) { CComPtr<IMMDeviceEnumerator> pEnum = NULL; CComPtr<IMMDeviceCollection> pCollection = NULL; UINT deviceCount = 0; try { // COMからMMDeviceEnumeratorを取ってくる throw_if_fail(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum))); // オーディオエンドポイントの列挙を実行 throw_if_fail(pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection)); // とれた数を数える throw_if_fail(pCollection->GetCount(&deviceCount)); for (int i = 0; i < deviceCount; i++) { try { // デバイスの情報を取る CComPtr<IMMDevice> pEndpoint = NULL; throw_if_fail(pCollection->Item(i, &pEndpoint)); // デバイスのプロパティを取る CComPtr<IPropertyStore> pProperties; throw_if_fail(pEndpoint->OpenPropertyStore(STGM_READ, &pProperties)); // 取ったプロパティから、値を指定して取得する PROPVARIANT vName; PropVariantInit(&vName); throw_if_fail(pProperties->GetValue(PKEY_Device_FriendlyName, &vName)); // エンドポイントの情報を取る CComPtr<IAudioEndpointVolume> pAudioEndVol = NULL; throw_if_fail(pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol)); // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい) float getVolume = 0.0f; throw_if_fail(pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume)); // 音量を取得 float setVolume = 0.25f; throw_if_fail(pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL)); // 音量を設定 std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n"; PropVariantClear(&vName); OutputDebugString(outString.c_str()); } catch (_com_error err) { OutputDebugString(L"デバイス情報取得のCOMエラー"); } } } catch (_com_error err) { OutputDebugString(L"デバイス列挙のCOMエラー"); } } // COMの後処理(COMのお作法) CoUninitialize(); return 0; }
これが、流れも違和感なくわかるし、一番すっきりしてよさそう。
今後、COMが出てくる箇所あれば、これでやってみようと思う。
備考
エラー処理の記述方法には、gotoを使う書き方もあるらしい。
https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com#jump-on-fail
が、個人的にgotoを使うのはなんかいやなので、これはナシとした。
参考
以前の記事(マイクスピーカーを扱うC++を書いたがメモリリークしまくり)