четверг, 26 марта 2009 г.

Что люди любят делать не так в IUnknown.QueryInterface

Это перевод The ways people mess up IUnknown::QueryInterface. Автор: Реймон Чен.

Когда вы имеете дело с вопросами совместимости приложений, каких только вещей, работающих только благодаря случайности, вы ни обнаруживаете! Сегодня я поговорю о некоторых "творческих" способах напутать с методом IUnknown.QueryInterface.

Сейчас вы должно быть подумали: "Этот интерфейс так важен для всего COM, как же можно сделать с ним что-то не так?". Ну-ну.

Забываем отвечать на IUnknown.
Иногда вы так взволнованы необходимостью отвечать на все свои прекрасные интерфейсы, что вы совершенно забываете о необходимости ответить на сам интерфейс IUnknown. Мы видели объекты, у которых вызов
var
  psf: IShellFolder;
  punk: IUnknown;
...
  psf := some object;
  psf.QueryInterface(IID_IUnknown, punk);
завершался неудачей с кодом E_NOINTERFACE!

Забываем отвечать на свой собственный интерфейс.
Есть методы, которые возвращают объект с конкретным интерфейсом. А если вы спросите у объекта его же собственный интерфейс (единственную причину для его существования), то он вам в ответ: "чего? О_о"
var
  psf: IShellFolder;
  peidl, peidl2: IEnumIDList;
...
  psf := some object;
  psf.EnumObjects(..., peidl);
  peidl.QueryInterface(IID_IEnumIDList, peidl2);
Некоторые объекты возвращают здесь E_NOINTERFACE из QueryInterface, хотя вы спрашиваете объект о нём самом!
Похоже они хотят сказать: "Извините, но я не существую".

Забываем отвечать на базовый интерфейс.
Когда вы реализуете интерфейс с наследованием, вы неявно реализуете и базовый интерфейс, так что не забывайте отвечать и на него тоже.
var
  psv: IShellFolder;
  pow: IOleView;
...
  psv := some object;
  psv.QueryInterface(IID_IOleView, pow);
А то некоторые объекты забывают об этом и возвращают E_NOINTERFACE из QueryInterface.

Требование секретного тук-тук.
Вообще-то, следующие два фрагмента кода эквивалентны:
var
  psf: IShellFolder;
  punk: IUnknown;
...
  CoCreateInstance(CLSID_xyz, ..., IID_IShellFolder, psf);
  psf.QueryInterface(IID_IUnknown, punk);

  CoCreateInstance(CLSID_xyz, ..., IID_IUnknown, punk);
  punk.QueryInterface(IID_IShellFolder, psf);
А в действительности, некоторые реализации лажают при втором вызове CoCreateInstance. Единственным способом создать объект успешно будет создать его через интерфейс IShellFolder.

Забываем правильно сказать "Нет".
Одним из правил при ответе "Нет" является требование установки выходного интерфейса в nil перед выходом. Некоторые забывают это делать
var
  pmbl: IMumble;
...
  punk.QueryInterface(IID_IMumble, pmbl);
Если вызов метода QueryInterface успешен, тогда pmbl должен быть не nil при выходе. А если вызов завершился неудачей, то pmbl обязан быть nil.

А ведь оболочка (shell) обязана быть совместимой со всеми этими багнутыми реализациями, потому что если она не будет совместима, то пользователи расстроятся, а пресса получит свою сенсацию. Некоторые из тех, кто не выполняет эти соглашения - программы С Большим Именем. Если они не будут работать в следующей версии Windows, люди будут говорить: "не обновляйтесь на Windows XYZ, она не совместима с <программа-с-большим-именем>!". А параноидально настроенные люди будут кричать: "Microsoft намеренно не даёт работать <программе-с-большим-именем>! Доказательство нечестной бизнес-тактики!".

10 комментариев:

  1. Вот же блин! Не знал, что IUnknown::QueryInterface используется не только в Delphi. O_O. А Delphi случайно не делает всё верно, скрывая от программиста описанное в этой статье?

    ОтветитьУдалить
  2. Случайно да. В Delphi аналогичную работу выполняет TObject.GetInterface - проверяя интерфейсы по списку. Соответственно, руками ничего делать не надо - достаточно, чтобы был объект, с наследованием от одного или нескольких интерфейсов.
    Вот и пример, как правильно построенный язык может избавлять от ошибок ;)
    Ну а вообще IUnknown со своим QueryInterface - это в первую очередь основа COM и к Delphi никакого отношения не имеет.

    ОтветитьУдалить
  3. Случайно нет. Описанное в "Забываем отвечать на базовый интерфейс" Delphi за Вас не сделает. Надо либо явно заявлять поддержку базового интерфейса, либо переопределять QueryInterface.

    А описанное в "Забываем правильно сказать "Нет"" не очень-то однозначно. Обнулять, оно конечно надо, но и функцию нефиг вызывать как процедуру. Тебе результатом HResult вернули - будь добр проверить.

    ОтветитьУдалить
  4. >>> Надо либо явно заявлять поддержку базового интерфейса

    Тогда такой вопрос: надо ли при этом вам писать код с чем-то вроде: "if AGUID = MyBaseInterface then"? Правильный ответ: не надо. По-моему, это и означает, что Delphi берёт это на себя, разве нет?

    ОтветитьУдалить
  5. Правильный ответ: надо.

    Delphi сделает для Вас только то, о чём Вы её явно попросите, и ничего более. Дабы избежать лишних слов, проиллюстрирую примером. Пусть у нас есть иерархия интерфейсов

    IA = interface(IUnknown)
    ...;
    IB = interface(IA)
    ...;
    с GUID, всё как положено. Пусть есть класс, реализующий IB:
    TB = class(TComObject, IB)
    и мы получили IUnknown экземпляра этого класса, например так
    var
    U: IUnknown;
    U:= TB.Create();
    или любым другим способом. Если мы теперь запросим через U интерфейс IB, то мы его получим, а вот попытка получить IA закончится неудачей - у нашего класса нет такого интерфейса, несмотря на то, что IB является потомком IA. Delphi ничего за нас не делает, и нам придётся либо явно прописать IA в объявлении класса
    TB = class(TComObject, IA, IB)
    либо в QueryInterface руками прописать именно то, что Вы и написали
    if EqualIID(IA, AGUID) then GetInterface(IB, obj);

    А если мы объявим так

    TB = class(TObject, IB)

    то не сможем получить у этого класса даже IUnknown - его просто нет в таблице интерфейсов этого объекта.

    ОтветитьУдалить
  6. >>> Правильный ответ: надо.

    Ткните пальцем, где в TB = class(TComObject, IA, IB) у вас стоит "if AGUID = IA then".

    ОтветитьУдалить
  7. TB = class(TComObject, IA, IB) - это ЯВНАЯ реализация двух интерфейсов, не имеющая отношения к наличию или отсутствию наследственности между ними.

    А в топике "Забываем отвечать на базовый интерфейс" однозначно сказано про НЕЯВНУЮ реализацию:
    "Когда вы реализуете интерфейс с наследованием, вы неявно реализуете и базовый интерфейс"

    Если-же Вам угодно заняться софистикой, то это - без меня.

    ОтветитьУдалить
  8. >>> Если-же Вам угодно заняться софистикой, то это - без меня

    Я не это имел ввиду. Я просто старался донести до вас мысль, что вы неверно прочитали мой ответ, и вам показалось, что я не в курсе про указанное вами поведение Delphi. Это не так.

    Позвольте, я процитирую сообщение, на которое вы отвечали, выделив ключевые части, а вы попробуете взглянуть на него с другой стороны: "Соответственно, руками ничего делать не надо - достаточно, чтобы был объект, с наследованием от одного или нескольких интерфейсов". Я имел ввиду: вам достаточно объявить объект со списком всех нужных интерфейсов, а всё остальное берёт на себя Delphi. Да, вы можете не запланировать поддержку интерфейса, но если вы про него сказали, то "пойти не так" уже ничего не может.

    ОтветитьУдалить
  9. Ну я не то чтобы ставил под сомнение Ваш уровень квалификации :) Скорее я был не вполне согласен с мыслью:
    "Вот и пример, как правильно построенный язык может избавлять от ошибок" Как раз данный случай - неявная реализация предков интерфейсов - является достаточно распространённой ошибкой среди новичков в COM. Люди зачастую так и поступают - объявляют реализацию верхнего в иерархии наследования интерфейса, и недоумевают, почему не удаётся получить от него интерфейс-предок. Однажды на одном весьма квалифицированном и известном форуме я видел длинную ветку с участием вполне квалифицированных людей, которые не могли найти причину, почему не удаётся создать объект, а причина была как раз в том, что объект был объявлен так
    TSome = class(TObject, ISomeInterface)
    и при его создании там пытались получить от него IUnknown. Причём мне даже не сразу удалось убедить участников, что причина именно в этом.

    Именно поэтому я счёл нелишним ещё раз обратить внимание на эту тонкость.

    ОтветитьУдалить
  10. > Именно поэтому я счёл нелишним ещё раз обратить внимание на эту тонкость.

    Да, это правильно. Все точки над i расставлены :)

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.