В мире разработки на Delphi работа с Windows API — это неотъемлемая часть многих проектов. Однако иногда даже опытные разработчики сталкиваются с неочевидными проблемами, связанными с особенностями имплементации интерфейсов COM. В этой статье мы разберем конкретный кейс, связанный с некорректным наследованием интерфейса IUnknown в сгенерированных модулях WinMD, и предложим рабочие решения.
Суть проблемы: конфликт объявлений IUnknown
Пользователь Memnarch столкнулся с неожиданным поведением при работе с интерфейсом IMMDeviceEnumerator из модуля Windows.Media.Audio.pas, который доступен через GetIt под названием "Windows API from WinMD". При вызове метода GetDefaultAudioEndpoint он не мог получить устройство по умолчанию, хотя аналогичный код, скопированный в его проект, внезапно начинал работать.
Ключевое наблюдение:
Исходный интерфейс IMMDeviceEnumerator в WinMD наследуется от Windows.Foundation.IUnknown, который объявлен так:
// Windows.Foundation.pas
IUnknown = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(riid: PGuid; out ppvObject: Pointer): HRESULT; stdcall;
function AddRef: Cardinal; stdcall;
function Release: Cardinal; stdcall;
end;
Однако в Delphi базовым интерфейсом для COM является System.IInterface (алиас IUnknown):
// System.pas
IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
Проблема:
Оба интерфейса имеют одинаковый GUID, но разную сигнатуру методов:
1. AddRef/Release возвращают Cardinal в WinMD vs Integer в System.
2. Параметр riid в QueryInterface — PGuid vs const TGUID.
Это приводит к смещению таблицы виртуальных методов (VMT). Например, метод GetDefaultAudioEndpoint в оригинальном IMMDeviceEnumerator ожидается на позиции 4 (после трех методов IUnknown), но из-за двойного наследования (через IInterface и Windows.Foundation.IUnknown) его фактическая позиция становится 7. Результат — вызов неверного метода или ошибка доступа.
Почему это критично? Технические детали
В COM все интерфейсы наследуются от IUnknown, и Delphi строго следует этому правилу через System.IInterface. При правильном объявлении VMT интерфейса выглядит так:
[0] QueryInterface
[1] AddRef
[2] Release
[3] EnumAudioEndpoints // Первый метод IMMDeviceEnumerator
...
Итог: Вызов EnumAudioEndpoints обращается к шестому элементу VMT, где находится _Release, что вызывает фатальную ошибку.
Решение 1: Использование исправленных модулей
Как справедливо заметил DelphiUdIT, WinMD содержит ряд ошибок. Вместо него можно использовать альтернативные источники, например, Win32 API имплементацию от WinSoft (https://www.winsoft.sk/win32api.htm), где интерфейсы правильно наследуются от System.IUnknown.
Пример корректного объявления:
// WinSoft реализация
IMMDeviceEnumerator = interface(IUnknown)
['{A95664D2-9614-4F35-A746-DE8DB63617E6}']
function EnumAudioEndpoints(...): HRESULT; stdcall;
function GetDefaultAudioEndpoint(...): HRESULT; stdcall;
// ...
end;
Решение 2: Локальное исправление интерфейса
Если замена модулей невозможна, скопируйте объявление интерфейса в свой код, исправив наследование:
type
IMMDeviceEnumerator = interface(IUnknown) // Наследуем от System.IUnknown
['{A95664D2-9614-4F35-A746-DE8DB63617E6}']
function EnumAudioEndpoints(dataFlow: EDataFlow; dwStateMask: Cardinal;
out ppDevices: IMMDeviceCollection): HRESULT; stdcall;
function GetDefaultAudioEndpoint(dataFlow: EDataFlow; role: ERole;
out ppEndpoint: IMMDevice): HRESULT; stdcall;
// ... остальные методы
end;
Важно: Убедитесь, что в uses отсутствует Windows.Foundation, чтобы избежать конфликта GUID.
Рабочий пример использования IMMDeviceEnumerator
Допустим, нам нужно получить устройство по умолчанию для воспроизведения звука. Вот как это сделать с исправленным интерфейсом:
uses
Winapi.Windows, Winapi.ActiveX, System.SysUtils;
procedure GetDefaultAudioDevice;
var
pEnumerator: IMMDeviceEnumerator;
pDevice: IMMDevice;
hr: HRESULT;
begin
// Инициализация COM
CoInitialize(nil);
try
// Создаем экземпляр IMMDeviceEnumerator
hr := CoCreateInstance(CLASS_IMMDeviceEnumerator, nil, CLSCTX_ALL,
IID_IMMDeviceEnumerator, pEnumerator);
if Failed(hr) then RaiseLastOSError;
// Получаем устройство по умолчанию
hr := pEnumerator.GetDefaultAudioEndpoint(eRender, eConsole, pDevice);
if Failed(hr) then RaiseLastOSError;
// Далее можно работать с pDevice...
WriteLn('Устройство получено успешно!');
finally
CoUninitialize;
end;
end;
Альтернативное решение: прямая работа с API через WinAPI
Если исправление интерфейсов кажется избыточным, используйте функции WinAPI напрямую:
const
CLSID_IMMDeviceEnumerator: TGUID = '{BCDE0395-E52F-467C-8E3D-C4579291692E}';
IID_IMMDeviceEnumerator: TGUID = '{A95664D2-9614-4F35-A746-DE8DB63617E6}';
function CoCreateInstance(const clsid: TGUID; unkOuter: IUnknown;
dwClsContext: Longint; const iid: TGUID; out pv): HResult; stdcall;
external 'ole32.dll';
procedure GetDefaultDeviceDirect;
var
pEnumerator: IMMDeviceEnumerator;
// ... остальные переменные
begin
CoCreateInstance(CLSID_IMMDeviceEnumerator, nil, CLSCTX_ALL,
IID_IMMDeviceEnumerator, pEnumerator);
// ... аналогично предыдущему примеру
end;
Заключение: что делать разработчику?
Проверьте источник API. Модули из WinMD могут содержать ошибки. Используйте проверенные альтернативы вроде WinSoft.
Локальные исправления. Если замены нет, копируйте интерфейсы в свой код с правильным наследованием.
Избегайте конфликта GUID. Следите за порядком модулей в uses, чтобы System.IUnknown имел приоритет.
Создайте тикет. Если вы обнаружили ошибку в официальных модулях Embarcadero, сообщите о ней через Quality Portal.
Ошибка с IUnknown в WinMD — яркий пример того, как автоматическая генерация кода может дать сбой. Однако благодаря гибкости Delphi и активному сообществу, решение всегда найдется.
Проблема наследования IUnknown в Delphi возникает из-за различий в объявлениях интерфейса между модулями WinMD и System, приводя к смещению таблицы виртуальных методов и ошибкам выполнения.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS