Карта сайта Kansoftware
НОВОСТИУСЛУГИРЕШЕНИЯКОНТАКТЫ
KANSoftWare

Проблема с наследованием IUnknown в Windows API для Delphi: ошибка или особенность реализации?

Delphi , Компоненты и Классы , Модули

 

В мире разработки на 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 в QueryInterfacePGuid 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
...

Но при "двойном" наследовании в WinMD:

[0] QueryInterface (System) → [3] QueryInterface (WinMD)
[1] AddRef (System)        → [4] AddRef (WinMD)
[2] Release (System)       → [5] Release (WinMD)
[3] _AddRef (System)       → ... 
[4] _Release (System)      → ... 
[5] EnumAudioEndpoints     → Смещение на +3!

Итог: Вызов 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;

Заключение: что делать разработчику?

  1. Проверьте источник API. Модули из WinMD могут содержать ошибки. Используйте проверенные альтернативы вроде WinSoft.
  2. Локальные исправления. Если замены нет, копируйте интерфейсы в свой код с правильным наследованием.
  3. Избегайте конфликта GUID. Следите за порядком модулей в uses, чтобы System.IUnknown имел приоритет.
  4. Создайте тикет. Если вы обнаружили ошибку в официальных модулях Embarcadero, сообщите о ней через Quality Portal.

Ошибка с IUnknown в WinMD — яркий пример того, как автоматическая генерация кода может дать сбой. Однако благодаря гибкости Delphi и активному сообществу, решение всегда найдется.

Создано по материалам из источника по ссылке.

Проблема наследования IUnknown в Delphi возникает из-за различий в объявлениях интерфейса между модулями WinMD и System, приводя к смещению таблицы виртуальных методов и ошибкам выполнения.


Комментарии и вопросы

Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS




Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.


:: Главная :: Модули ::


реклама


©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007
Top.Mail.Ru

Время компиляции файла: 2024-12-22 17:14:06
2026-01-13 02:35:15/0.029434204101562/0