Гергерт Сергей
Работа с 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.