dispatchability

Персональный блог одноименного форумчанина. Человека и парохода, не побоюсь этого сравнения :)

Модератор: tyomitch

tyomitch
Пользователь #1352
Пользователь #1352
Аватара пользователя
 
Сообщения: 12822
Зарегистрирован: 20.10.2002 (Вс) 17:02
Откуда: חיפה

dispatchability

Сообщение tyomitch » 11.08.2006 (Пт) 12:48

В прошлый раз я остановился на связывании методов по виртуальной таблице (vtable binding): в традиционных ОО-языках его считают поздним связыванием, потому что выбор конкретной реализации метода осуществляется во время выполнения; в COM и VB6 его считают ранним связыванием, потому что на этапе компиляции необходимо знать смещения указателей на каждый метод в виртуальной таблице, т.е. во время компиляции должен быть известен класс любого из предков объекта, включающего вызываемый метод.

* Второй основой COM, после виртуальных методов, является интерфейс IDispatch, который позволяет работать с объектами, тип которых неизвестен заранее. Чтобы вызывать методы класса, реализующего этот интерфейс, достаточно знать их имена. Метод IDispatch::GetIDsOfNames возвращает по имени метода его DispID -- 4-байтный идентификатор, совпадающий по смыслу с идентификатором динамических методов в Delphi. DispID метода должен сохраняться неизменным на всём времени жизни объекта, возвратившего его по вызову GetIDsOfNames. Другой метод, IDispatch::Invoke, вызывает произвольный метод объекта по DispID. Аргументы для этого метода передаются в Invoke как массив типа Variant.

Единая точка входа для всех методов класса обеспечивает такую же гибкость, которая в прошлый раз описывалась применительно к "полувиртуальным" методам. Метод Invoke может содержать произвольную логику, управляющую соотнесением DispID и конкретной реализации метода: он может менять обработчик во время выполнения, может передавать вызов на обработку классу-предку в зависимости от каких-либо условий и т.д. Фактически, связывание методов dispatchable-классов полностью перенесено с компилятора на объекты этих классов.

Наследование dispatchable-классов осуществляется в лучших традициях "маршрутизации сообщений" в Smalltalk. В отличие от наследования обычных классов, внешний интерфейс полностью образуется классом-потомком: классы-предки в него не "вмонтируются", как в предыдущих случаях. Вместо этого объекты-предки "агрегируются" вглубь потомка, как это обычно при реализации "наследования" в VB6, и реализации методов IDispatch класса-потомка просто внутри себя вызывают методы предков для всех методов, которые в этом классе-потомке не переопределены. Таким образом, каждый вызов метода класса с большим деревом предков последовательно маршрутизируется по ветвям этого дерева, пока не дойдёт до нужного узла. Поскольку здесь при множественном наследовании не создаётся ни кучи виртуальных таблиц, ни кучи корректоров, как в предыдущих примерах -- IDispatch предоставляет идеальный механизм для множественого наследования. С определёнными ухищрениями типа замены стандартной реализации IDispatch на собственную, это множественное наследование становится доступным и для классов VB6.

Соответственным образом преображается применительно к dispatchable-классам понятие COM-интерфейса: для классов со связыванием по VTbl интерфейс (в понятиях C++ -- чисто абстрактный (pure abstract) базовый класс) -- это виртуальная таблица, которую класс должен обязательно включать. Например, любой COM-класс обязан поддерживать интерфейс IUnknown, т.е. у любого COM-класса должна быть виртуальная таблица, совпадающая с виртуальной таблицей IUnknown. Для dispatchable-классов вместо понятия "интерфейс" применяется "диспинтерфейс" (dispinterface): это набор DispID, которые должен поддерживать класс. У многих отрицательных DispID есть специальные значения: наиболее известный из них -- DISPID_NEWENUM = -4 -- соответствует методу, возвращающему перечислитель для класса-коллекции; менее известный DISPID_AMBIENT_MESSAGEREFLECT = -706 соответствует свойству, возвращающему True, если класс является ActiveX-контролом, поддерживающим отражение сообщений. Нулевой DispID соответствует "свойству по умолчанию".

Dispatchable-классы в COM удивительно схожи с оконными процедурами: точно так же, как все обработчики оконных сообщений для одного окна, все методы одного объекта имеют общую точку входа, которой передаётся числовой идентификатор вызываемого метода и набор универсальных параметров, конкретный смысл которых меняется от одного метода к другому. Как и для окон, для dispatchable-классов легко создать подкласс, перехватив вызов Invoke и в зависимости от переданных параметров либо возвращая управление исходному обработчику, либо осуществляя собственную обработку.

Большинство dispatchable-классов являются двойными (dual), т.е. они содержат виртуальную таблицу методов, и поддерживают связывание как через VTbl ("раннее"), так и через IDispatch ("позднее"). К таким двойным классам относятся, в частности, все классы, созданные в VB6. Использование двойных классов редко бывает оправдано -- в средах, поддерживающих только позднее связывание (например, VBS), безполезной оказывается VTbl; в средах, поддерживающих раннее связывание, нет нужды в поддержке IDispatch. Зато в COM есть стандартная функция CreateStdDispatch, которая для обычного COM-класса создаёт IDispatch-обёртку, тем самым превращая этот класс в двойной; поэтому разработчику не приходится реализовывать методы IDispatch самому.

* Самый неэффективный способ работы с dispatchable-классами, который только можно придумать -- это связывание по имени; и именно оно используется в VB6 под названием "позднего связывания" -- для типов Object и Variant. При этом каждый вызов метода объекта компилируется в пару вызовов GetIDsOfNames и Invoke. Это означает, что при каждом вызове по-новой (при помощи сравнений строк!) ищется DispID вызываемого метода. Более продвинутые реализации IDispatch могут хранить кэш нескольких последних полученных DispID, и тогда при вызове одних и тех же методов в цикле потери времени на связывание будут чуть меньше; но в общем у этого способа связывания есть только одно преимущество -- он тривиально реализуется.

* Более эффективный способ связывания -- "позднее связывание по DispID", который, судя по документации COM, задумывался как основной способ позднего связывания. В этом случае DispID методов класса по мере их получения кэшируются не самим объектом, а его пользователем; при первом вызове метода объекта выполнются оба метода GetIDsOfNames и Invoke, при всех последующих -- только Invoke. Этот способ связывания тяжело реализовать: поскольку DispID принадлежат не классу, а конкретному объекту, то нужно либо хранить какую-то огромную хеш-таблицу с DispID-ами всех объектов (тогда необходимость поиска объекта в этой хеш-таблице сводит на нет выгоду от кэширования), либо хранить кэш для каждой объектной переменной, и каким-то образом отслеживать, когда объект в этой переменной меняется (в этом случае нужно сбрасывать кэш). В общем, при ручном кодировании доступа к dispatchable-классам этот способ связывания ещё осуществим, но при генерации кода из программы на ЯВУ, например на VB6, такая мега-оптимизация граничит с искуственным интеллектом.

* Следующий способ связывания -- "раннее связывание по DispID", когда интерфейс dispatchable-класса известен на этапе компиляции, но этот класс не является двойным (иначе можно было бы применить связывание по VTbl). Узость применимости этого типа связывания ограничивает его средами, которые поддерживают TLB, но не поддерживают вызов методов по VTbl; вроде бы есть такие скриптовые языки, хотя я не знаю ни одного конкретного примера.

В VB6 раннее связывание по DispID применяется для не-двойных dispatchable-классов, а также для интерфейсов контролов и событий. Поэтому, например, использование callback-интерфейса работает эффективнее, чем стандартный механизм событий.

* Последний мыслимый способ работы с dispatchable-классами -- "позднее связывание по VTbl" -- использует метод IDispatch::GetTypeInfo, который возвращает информацию об интерфейсе, реализованном классом. Если этот класс -- двойной, то в описании интерфейса будут имена и смещения внутри VTbl всех его методов. Получается, что можно один раз, при первом использовании объекта, запросить это описание, и сразу же "докомпилировать" все последующие обращения к объекту так, чтобы они шли через VTbl. Этот чудо-способ сочетает гибкость позднего связывания и эффективность раннего; но я не слышал, чтобы он вообще хоть где-нибудь применялся.
Изображение

Вернуться в Tyomitch

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 7

    TopList  
cron