Дата публикации статьи: 22.05.2004 11:58

Гергерт Сергей
Работа с COM-объектами, не поддерживающими IDispatch

    Эта статья предполагает, что вы знакомы с функциями Get/PutMem и вызовом функций по указателю (Подробнее...).
    Как мы все знаем, VB всё делает за нас при работе с объектами. После:

Dim obj As Object
Set obj = something

    Можно просто вызывать методы: obj.method1, obj.method2 - VB справляется с такими вызовами, даже если у него нет описания данного объекта. Как у него это получается? Очень просто. У него это не получается :) Всё дело в том, что Object в VB - это всегда такой объект (COM-объект), который реализует интерфейсы IUnknown и IDispatch. Через IUnknown VB спрашивает: "Поддерживаешь IDispatch?". "Yep...". VB переходит на общение по IDispatch. "Есть метод по имени 'method1'?". "Есть!". "Параметры?". "Такие-то!". "Ага, совпадает... Тогда лови!..". Не будем говорить о том, сколько времени уходит на подобные диалоги, ни к чему это :) За лёгкость надо платить ;)
    Но существует куча прекрасных объектов, реализующих IUnknown (его реализуют абсолютно все COM-объекты), но не реализующих IDispatch! Диалог с такими объектами либо завершается на стадии "Поддерживаешь IDispatch?". "Oops...", либо приводит к относительно допустимому сворачиванию коврика. Не имея механизма опроса объекта на предмет его методов, VB делает вывод, что работать с ним ну никак нельзя. Но когда это нас останавливало встроенное ограничение? :) Начнём с основ, чтобы не создавать смутного и невразумительного впечатления ;)
    Вот создали мы описание класса (в VB или ещё где - не суть). Вот создали экземпляр этого класса, и ещё один - да много насоздавали мы экземпляров... У каждого экземпляра свои внутренние данные, каждый обладает методами. Так вот, данные у каждого экземпляра действительно свои, а вот методы у всех экземпляров общие. Какой бы экземпляр не вызвал метод - вызов пойдёт в одну и ту же точку кода. Что, в общем, логично - откомпилированный код процедуры не должен изменяться (хотя можно пошалить и тут), зачем же его клонировать. А раз у всех методы общие, значит, каждый должен иметь представление о том, где эти общие находятся. Это представление называется vTable. Это такая таблица (одномерный массив, если угодно), каждый элемент которого имеет размер 4 байта и является указателем на метод. Все методы, поддерживаемые объектом, идут в этой таблице подряд. Упорядочены они по интерфейсам (сначала все методы одного интерфейса, потом все другого...), а в рамках конкретного интерфейса порядок методов определяется порядком описания этих методов в исходнике. Первым идёт всегда интерфейс IUnknown, а его методы - всегда в порядке: QueryInterface, AddRef, Release. Только так. Всегда и везде. Благодаря этим трём фиксированным указателям, этим 12 байтам, собственно, и живёт технология COM :)
    Но мы же уже умеем вызывать по указателю, через cFuncCall! Осталось подкорректировать совсем немного. Как получить указатель на vTable? Очень просто. Интерфейс - это и есть указатель на vTable. Как получить интерфейс? Очень просто. Вызвав функцию CoCreateInstance. Она создаст объект и вернёт запрашиваемый интерфейс объекта. Всё, что нужно знать для этого - GUID объекта и интерфейса (уникальный идентификатор, имеющийся у каждого COM-объекта). Где его посмотреть? В документации (лучше всего), в файлах .h (если есть Visual Studio) или в реестре (если времени много и делать особо нечего). Нашли GUID-строки. Куда их? В функции

Public Declare Function CLSIDFromString Lib "ole32.dll" (ByVal lpsz As String, pclsid _
As modOLECommon.Guid) As Long
Public Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As String, lpiid _
As modOLECommon.Guid) As Long

    Эти функции парсят строку и заполняют на её основе структуру GUID (как нетрудно видеть, она у меня определена в модуле modOLECommon. См. код).
GUID создали, интерфейс получили. Дальше что?

GetMem4 pInterface, VarPtr(vTable)     'дереференс: находим положение vTable

    И вот мы уже в начале таблицы. Дальше? А дальше идём в мануал. И в этом мануале читаем описание этого интерфейса. Если нет мануала, лезем в .h. В общем, узнаём, каким же по счёту идёт интересный нам метод. Узнали? Метод номер 6? Методы нумеруются с нуля! Так что метод номер 5. Чтобы больше не лезть никогда в этот мануал, определяем константу типа

Const MYINTERAFACE_MYMETHOD As Long = 5

    Метод номер 5. Размер указателя 4 байта. А мы в начале таблицы. А они все подряд. Так что, само собой,

GetMem4 vTable + MYINTERAFACE_MYMETHOD * 4, VarPtr(MyMethodPointer)

    Только не сразу вызываем, не сразу... У каждого метода интерфейса есть ещё один параметр - указатель на сам этот интерфейс. В C он называется this, в Дельфи - Self, у нас - Me. А раз он присутствует каждый раз, то что мешает нам немного видоизменить cFuncCall.CallFunction, чтобы учесть эту особенность? Полученную CallInterface помещаем во всё тот же modOLECommon.
    Как потом построить работу с интерфейсами - решаете в каждом конкретном случае. Можно остановиться на простых вызовах CallInterface, а можно создать класс, методы которого будет это делать (тогда не придётся лазить в мануал и смотреть хэлп по параметрам). Прилагаемый ShellLink построен по второму принципу - но и первый имеет право на существование, а порой и более эффективен.
    Ну вот, собственно и всё - теперь можно работать с интерфейсами. Можно вручную вызывать методы AddRef и Release, управляя временем жизни объектов. Можно общаться со стандартными интерфейсами Windows, коих очень много. Можно даже с DirectX работать таким вот способом :) Хоть смайл и стоит, а не шучу :) Ибо, скажу я вам, далеко не все фишечки DirectX реализованы под VB. А если через CoCreateInstance - функциональность полная...
Да! Каждый процесс, хотящий работать с OLE, должен предварительно вызывать OleInitialize. И на каждый такой вызов должен быть соответствующий OleUninitialize - при завершении работы. Достаточно одного вызова.
modOLECommon появился, я надеюсь? ;)

Скачать проект ShellLink.