Перейти к содержимому

Фотография

Простыми словами о Com

- - - - -

  • Авторизуйтесь для ответа в теме

#1
daimond

Отправлено 15 ���� 2008 - 05:08

daimond

    Свояк

  • Пользователи
  • 232 сообщений
Хотел я в общих словах расказать основную идею СOM. Когда я понял, что это такое показалась такая простая вещь, что можно коротко рассказать. Но не получилось... Что ж, будет немного подлиннее. Итак, попробуем рассказать в простоте. Вот есть у вас класс - примитивный калькулятор: MyCalc=class fx,fy:integer; public: procedure SetOperands(x,y:integer) function Sum:integer; function Diff:integer; end; procedure MyCalc.SetOperands(x,y:integer) begin fx:=x; fy:=y; end; function MyCalc.Sum:integer; begin result:=fx+fy; end; function MyCalc.Diff:integer; begin result:=fx-fy; end; Все элементарно. Теперь если у вас есть объект этого класса, то вам не составит труда им воспользоваться. Но представим следующую ситуацию: у вас есть один модуль, где объявлется объект этого класса. Допустим: Исходный код unit MyCalc type MyCalc=class <описание выше> var Calc:MyCalc; и теперь вы хотите использовать в другом модуле. Хорошо, скажите Вы, мы его просто подключим, и используем. Но, допустим, вы хотите, чтобы и другие могли пользоваться вашим объектом, даже используея другой компилятор. То есть нужно сделать так, чтобы ваш модуль можно было бы использовать без перекомпиляции. Как это сделать? Ясно, что без каких-то стандартов не обойтись. Скорее всего, самый простой вариант выглядел бы так: Исходный код unit MyCalc type MyCalc=class <описание выше> var Calc:MyCalc; procedure SetOperands(x,y:integer) begin Calc.SetOperands(x,y); end; function Sum:integer; begin result:= Calc.Sum; end; function Diff:integer; begin result:= Calс.Diff; end; procedure CreateObject; begin Calc:=MyCalc.Create; end; procedure ReleaseObject; begin Calc.Free; end; откомпилировать этот юнит, посмотреть, по какому адресу находятся функции SetOperands, Sum, Diff, CreateObject и ReleaseObject и приложить документацию где эти адреса будут указанны. Теперь каждый сможет загрузить ваш модуль в память и по адресу указанном в вашей документации вызвать нужную функцию. Понятно, чем такой подход череват. Это крайне не удобно. Но, эта проблема была поставленна давно, и теперь у нас есть стандартизированное соглашение об экспорте функций. То есть вместо того, чтобы писать для каждого модуля документацию с адресами функций при компиляции в заголовке модуля создается специальная стандартная таблица где указанны имена этих функций и их адреса (также указывается числовой индефикатор, который может быть использован вместо имени). Теперь уже лучше. Для того чтобы вызвать ваши функции, достаточно загрузить ваш модуль в память прочитать таблицу экспорта, и можно по именам в ней нати адреса функций и их вызвать. Так устроены DLL. Сейчас все это поддерживается компиляторами, и Windows API. То есть вам самому ничего этого делать не надо, а достаточно вызвать LoadLibrary, чтобы загрузить ваш модуль в память, и GetProcAddress чтобы получить адрес функции по имени. Ура. До нас и за нас все уже стандатизировали. Давайте этим воспользуемся и напишим теперь наш модуль в постандарту. Напишим dll. Исходный код library CalcDll; uses SysUtils, Classes; type MyCalc=class fx,fy:integer; public procedure SetOperands(x,y:integer); function Sum:integer; function Diff:integer; end; var Calc:MyCalc; procedure MyCalc.SetOperands(x,y:integer); begin fx:=x; fy:=y; end; function MyCalc.Sum:integer; begin result:=fx+fy; end; function MyCalc.Diff:integer; begin result:=fx-fy; end; procedure SetOperands(x,y:integer); begin Calc.SetOperands(x,y); end; function Sum:integer; begin result:=Calc.Sum; end; function Diff:integer; begin result:=Calc.Diff; end; procedure CreateObject; begin Calc:=MyCalc.Create; end; procedure ReleaseObject; begin Calc.Free; end; exports //Вот эта секция и указывает компилятору что записать в таблицу экспорта SetOperands, Sum, Diff, CreateObject, ReleaseObject; begin end. Напишим программку - протестировать наш модуль. Исходный код unit tstcl; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; _Mod:Integer; //индефикатор модуля SetOpers:procedure(x,y:integer); //Это все указатели на функции, diff,sum:function:integer; //которые мы собираемся получить CreateObj,ReleaseObj:procedure; //из нашего модуля implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin //загружаем наш модуль в память _Mod:=LoadLibrary('CalcDll.dll'); //получаем адреса функций по именам CreateObj:=GetProcAddress(_Mod,'CreateObject'); ReleaseObj:=GetProcAddress(_Mod,'ReleaseObject'); sum:=GetProcAddress(_Mod,'Sum'); diff:=GetProcAddress(_Mod,'Diff'); SetOpers:=GetProcAddress(_Mod,'SetOperands'); CreateObj; //вызываем функцию по адресу SetOpers(13,10); //вызываем функцию по адресу end; procedure TForm1.FormDestroy(Sender: TObject); begin ReleaseObj; //опять вызываем функцию по адресу FreeLibrary(_Mod); //выгружаем модуль из памяти end; procedure TForm1.Button1Click(Sender: TObject); begin ShowMessage(IntToStr(diff)); //вычисляем разницу end; procedure TForm1.Button2Click(Sender: TObject); begin ShowMessage(IntToStr(sum)); //вычисляем сумму end; end. Классно! Теперь каждый программирующий в системе Windows на любом языке может использовать наш калькулятор! Что? Разочарованны? Такое ощущение что COM тут и не пахнет? Правильно, ибо про СОМ я пока ничего и не сказал, но/// Сделаем еще шаг в направлении Component Object Module (COM).Даже сейчас у экспортируется довольно много функций. Соответсвенно и в программе нам надо сделать несколько ступений - создать переменную-указатель, присвоить ей значение адреса нужной функции при помощи GetProcAddress, и только потом вызвать саму функцию. Причем все эти функции у нас сами по себе и никак не связанны с самим объектом, который мы используем. А неплохо бы сделать так, чтобы можно было работать с ними как с объектом, что нибудь типа: Сalc.SetOperands(13,10); i:=Calc.Sum; Так давайте так и сделаем! Правда мы ограничены экспортом только функций, но мы сделаем так: Добавим в dll такую запись Исходный код type ICalc=record SetOpers:procedure (x,y:integer); Sum:function:integer; Diff:function:integer; Release:procedure; end; и процедуру: procedure GetInterface(var Calc:ICalc); begin CreateObject; Calc.Sum:=Sum; Calc.Diff:=Diff; Calc.SetOpers:=SetOperands; Calc.Release:=ReleaseObject; end; и будем экспортировать только ее: exports GetInterface; Видете что происходит? Теперь вместо того, чтобы получать адрес каждой функции, мы можем получить сразу всю таблицу адресов. Причем создание объекта происходит в этой же функции, и пользователю больше не нужно знать функцию CreateObject и не забыть ее вызвать. Переделаем наш тестер. В описание типов добавим: Исходный код type ICalc=record SetOpers:procedure (x,y:integer); Sum:function:integer; Diff:function:integer; Release:procedure; end; изменим секцию var. Исходный код var Form1: TForm1; _Mod:Integer; GetInterface:procedure (var x:ICalc); Calc:ICalc; и процедуры где мы используем наш объект. Исходный код procedure TForm1.FormCreate(Sender: TObject); begin _Mod:=LoadLibrary('CalcDll.dll'); GetInterface:=GetProcAddress(_Mod,'GetInterface'); GetInterface(Calc); Calc.SetOpers(13,10); end; procedure TForm1.FormDestroy(Sender: TObject); begin Calc.Release; FreeLibrary(_Mod); end; procedure TForm1.Button1Click(Sender: TObject); begin ShowMessage(IntToStr(Calc.diff)); end; procedure TForm1.Button2Click(Sender: TObject); begin ShowMessage(IntToStr(Calc.Sum)); end; Теперь со стороны может показаться, что мы пользуемся объектом, хотя на самом деле это всего лиш таблица с указателями на функции. Тут наконец проявляется одно из ключевых понятий COM - интерфейс(interface). Наша запись ICalc - это он и есть. То есть интерфейс - это таблица содержашаяя указатели на функции. Когда вы работаете с COM объектом, несмотря на то, что это выглядит так, как будто вы работаете с самим объектом, вы работаете с его интерфейсами. Реализация здесь может быть разная, это может быть указатели на внешнии функции, как это сделанно у нас (так практическм никто не делает), но чаще всего это указатели на методы класса. Пользователя это не волнует - он получает интерфейс и с ним работает, а уж ваша задача потрудиться над тем, чтобы работа с вашим интерфейсом проходила корректно. Мы можем создать несколько интерфейсов. Допустим, добавим в наш класс две функции: Исходный код procedure MyCalc.Mult; //умножение begin result:=fx*fy; end; procedure MyCalc.Divide; //деление begin result:=fx div fy; end; ну и придется добавить еще две внешнии функции: Исходный код procedure Mult; begin Calc.Mult end; procedure Divide; begin Calc.Divide; end; и переделаем GetInterface; Исходный код procedure GetInterface(IID:integer; var Calc:ICalc); //IID - Interface ID(индефикатор интерфейса) begin CreateObject; if IID=1 then begin Calc.Sum:=Sum; Calc.Diff:=Diff; end else If IID=2 then begin Calc.Sum:=Mult; Calc.Diff:=Divide; end; Calc.SetOpers:=SetOperands; Calc.Release:=ReleaseObject; end; Теперь пользователь может ввести какой он хочет интерфейс сложение/вычитание или умножение/деление и получить соответсвующую таблицу методов. Читатели уже начинают замечать сколько несуразностей в нашем коде. Давайте попробуем приводести его в нормальный вид. Во первых, неплохо бы избавиться от внешних функций. Сейчас нам приходилось на каждый метод нашего класса добавлять еще одну внешнюю функцию, чтобы этот метод вызывать. Почему мы не можем передпть указатели на методы класса? Дело в том, что указатель на методы класса должен содержать в себе также и указатель на экземпляр класса, чтобы метод мог обращаться к членам этого класса. В Делфи можно задать указатель на функцию класса: MethodPointer:procedure of object; Такое обявление увеличивает размер указателя с 4 до 8 байт, что позволяет хранить в нем указатель на экземпляр класса. В принципе, возможно этим воспользоваться и описать процедуры нашего интерфейса как объектные, но это не будет шаг в сторону COM. Так как COM должен обеспечивать единый стандарт в нем используются указатели стандартного размера 4 байта. Как же нам все-таки избавиться от неудобных внешних функций? В разных средах разработки это может быть реализованно по разному, но раз уж мы начали с Delhpi, рассмотрим как это реализованно в нем. В Delphi вводиться ключевое слово - interface. Объявление инерфейса - это и есть объявление таблицы методов. Выглядит это так IMyInterface=interface [{GUID}] <метод1> <метод2> ... end; GUID - необязательное поле индефицируеющая интерфейс. Тут надо сказать, что GUID(он же UUID, CLSID) - это 128-битное число, алгоритм генерации которого гарантирует его уникальность во вселенной. В Windows его можно получить функцией CoCreateGuid или UuidCreate. В Делфи это очень удобно встроенно в среду, и вы его можете получить нажав Ctrl+Shift+G. В нашем простом случае это будет выглядить так: Исходный код ICalc=interface ['{149D0FC0-43FE-11D6-A1F0-444553540000}'] procedure SetOperands(x,y:integer); function Sum:integer; function Diff:integer; procedure Release; end; Объявленный таким образом интерфейс можно прицепить к классу. Причем заметье, что методы интерфейса имплементируются только в классе, к которому они прицеплены. То есть вот так вы написать не можете: function ICalc.Sum:integer; begin Result:=0; end; Как и было сказанно, объявление интерфейса это всего лишь объявление таблицы методов. А имплементируется это так: Исходный код MyCalc=class(TObject,ICalc) //интерфейс указывается в списке наследования! fx,fy:integer; public procedure SetOperands(x,y:integer); function Sum:integer; function Diff:integer; procedure Release; end; Все методы класса у нас уже имплементированны, кроме Release. Ну с ним все понятно: procedure MyCalc.Release; begin Free; end; По умолчанию, методы привязываются по именам. То есть если в ICalc указан метод Sum, то компилятор будет искать метод Sum в классе MyCalc. Однако вы можете указать явно другие имена. Например: Исходный код MyCalc=class(TObject,ICalc) fx,fy:integer; public function ICalc.Diff = Difference; //задаем нужнок имя (Difference) procedure SetOperands(x,y:integer); function Sum:integer; function Difference:integer; //другое имя procedure Release; end; В нашем случае, удобно промаппить метод Release к методу Free, это избавит нас от необходимости имплементировать Release в нашем классе. Исходный код MyCalc=class(TObject,ICalc) fx,fy:integer; public function ICalc.Release = Free; procedure SetOperands(x,y:integer); function Sum:integer; function Diff:integer; end; Что же происходит при добовлении к классу интерфейса? Здесь для каждого экземпляра нашего класса создается специальная таблица(interface table), в которой храняться все записи о поддерживаемых интерфейсах. Каждая такая запись содержит адрес соответствующего интерфейса, который в свою очередь, как уже было сказанно является таблицей методов. То есть если мы получим адрес, допустим, нашего ICalc, то вызывая функцию по этому же адресу, мы вызовем метод SetOperands класса MyCalc. Ecли вы вызовете вызовете функцию по адресу <адрес ICalc>+4 то вызовется метод Sum. Еще +4 байта будет метод Diff. То есть как вы видете, здесь указатели на функции имеют размер 4 байта, и адрес нужной функции получают прибавлением нужного смещения к адресу интерфейса. Получить же адрес нужного интерфейса можно с помощью метода GetInterface класса TObject. Забудем пока, что мы делали два интерфейса, и вернмся к варианту с одним интерфейсом. Перепишим наш GetInterface. Исходный код procedure GetInterface(var ACalc:ICalc); begin CreateObject; Calc.GetInterface(ICalc,ACalc); end; Мы воспользовались методом GetInterface, который вышлядит так: function TObject.GetInterface(const IID: TGUID; out Obj): Boolean; этот возвращает в параметре Obj указатель на интерфейс, по указанному индификатору GUID. Допускается вместо переменной типа TGIUD поставить имя интерфейса - компилятор сам подставит его GUID если он ему известен. Все. Выбрасывайте все внешнии функции, кроме GetInterface. Теперь нам придется сказать спасибо Borland'у и сделать несколько дополнительных действий. Дело в том, что по стандарту COM каждый COM объект должен имплементировать интерфейс IUnknown. Он содержит три метода и выглядит так: IUnknown = interface ['{00000000-0000-0000-C000-000000000046}'] function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end; Хочу еще раз отметить, что эти примеры пишутся для Делфи, однако суть от этого не меняется. Как бы не выглядил интерфейс в других средах разработки, он всегда остается таблицой с адресами функций. И если говорить о IUnkown, то он всегда должен содержать эти же методы, в этом же порядке. В С++ он например выглядит так: struct IUnknown { HRESULT QueryInterface(REFIID iid, void ** ppvObject); ULONG AddRef(void); ULONG Release(void); } Так вот, в Delhpi все интерфейсы наследуются от IUnknown. Так что и наш интерфейс тоже содержит эти методы, а значит и компилятор потребует от вас их имплементации. Ну что ж. Добавтье пока пустые методы QueryInterface, _AddRef и _Release, позже мы их имплементируем правильно. Теперь не забудтье поменять тип ICalc на интерфейс в тестере, и убедитесь, что все работает. Давайте теперь вернем наш второй интерфейс. Теперь, когда таблица интерфейсов создается компилятором, мы не можем эмулировать два интерфейса простой заменой адресов функций. Теперь нам действительно надо создать второй интерфейс. Хорошо, что интерфейсы можно наследовать: Исходный код ICalc2=interface(ICalc) function Mult:integer; function Divide:integer; end; Tак ICalc2 будет содержать в себе все методы ICalc. Нам Sum и Diff в этом интерфейсе не нужны, так что давайте лучше напишим так: Исходный код ICalcBase=interface //здесь нам GUID не нужен, так как с этим интерфейсом мы работать не собираемся. procedure SetOperands(x,y:integer); procedure Release; end; ICalc=interface(ICalcBase) ['{149D0FC0-43FE-11D6-A1F0-444553540000}'] function Sum:integer; function Diff:integer; end; ICalc2=interface(ICalcBase) ['{D79C6DC0-44B9-11D6-A1F0-444553540000}'] function Mult:integer; function Divide:integer; end; Теперь добавим его в наш объект. MyCalc=class(TObject,ICalc,ICalc2) ... //без изменений function Divide:integer; //это и function Mult:integer; //это добавили end; Опять возмемся за наш GetInterface. В принципе, мы могли бы оставить выбор интерфейса как было у нас раньше - передаем в GetInterface целую переменную и если она равна 1 то возвращаем ICalc, а если 2 то ICalc2. Но уж коли мы связались с COM, то давайте будем, по возможнсти, к нему приближаться. Сделаем полную аналогию GetInterface в TObject: Исходный код function GetInterface(const IID: TGUID; var ACalc): Boolean; begin CreateObject; Result:=Сalc.GetInterface(IID,ACalc); if not Result then Calc.Free; end; Вуаля! Чуствуется, насколько теперь лучше, чем было вначале? Теперь если запрашиваемый инерфейс нашим объектом не поддерживается, то во-первых, мы даем клиенту об этом узнать, возвращая в Calc nil ( TObject.GetInterface это делает) и возвращая False из функции, а во- вторых, мы сразу же освобождаем объект. Но на самом деле, то что во-вторых, ничего хорошего нет, ибо мы подходим к следующей проблеме. Функцию мы обозвали GetInterface, но она еще и объект создает! А если пользователь захотел получить вначале ICalc, а потом ICalc2? Так как ему известна лишь функция GetInterface, он может воспользоваться только ей и получит два объекта, вместо двух интерфейсов одного объекта. Значит нужно отделить функции создание объекта, от получение его интерфейса. Давайте попробуем это сделать. Первая попытка: Исходный код ... var Calc:MyCalc; //без изменений ... ... procedure CreateObject; begin Calc:=MyCalc.Create; end; function GetInterface(const IID: TGUID; var ACalc): Boolean; begin Result:=Сalc.GetInterface(IID,ACalc); end; exprots CreateObject, //добавили в экспорт GetInterface; Хм... Не работает, не правда ли? Если клиент сделает так: CreateObject; CreateObject; GetInterface(ICalc, Calc); то он получит интерфес второго созданного объекта, тогда как первый объект будет навсегда утерян. Что же надо сделать? Надо сделать так, чтобы CreateObject возвращала бы чего-нибудь, чтобы мы могли индифецировать объект, и получать имено его интерфейсы. Как я уже сказал, клиент работает с COM объектом только через его интерфейсы, значит логичнее всего при создании объекта вернуть интерфейс созданного объекта(точнее, указатель на него). Для нашего случая, можно возвращать указатель на ICalc, но можно облегчить жизнь ползователю, и попросить его указать, какой интерфейс он хочет. Исходный код procedure CreateObject(const IID: TGUID; var ACalc); var Сalc:MyCalc; begin Calc:=MyCalc.Create; if not Calc.GetInterface(IID,ACalc) then Calc.Free; end; Здесь если интерфейса, который пользователь попросит нас нет, мы вернем nil и удалим объект. Если интерфейс есть, то пользователь сам будет удалять объект через метод Release. Неплохо, не правда ли? Теперь глобальная переменная Calc нам не нужна - мы создаем много обектов динамически Ну теперь совсем очевидно, что если ползователь захочет еще один интерфейс этого объекта, то логичнее всего у этого объекта этот интерфейс и поросить. Вот мы уже влотную подошли к имплементации IUnknown - основного интерфейса в COM. Как я уже сказал, все объекты должны имплементировать IUnknown, и все интерфейсы должны быть потомками IUnknown(что Borland и сделал). Так что вы помните, что и оба наших интерфейса ICalc и ICalc2 являются потомками IUnknown, а значит и первые три метода, которые они содержат - это QueryInterface, AddRef, Release. Помните, я предлагал вам оставить эти три метода пустыми? Давайте сейчас имплементируем один из них - QueryInterface: Исходный код function MyCalc.QueryInterface(const IID: TGUID; out Obj): HResult; begin if GetInterface(IID, Obj) then Result := S_OK; else Result := E_NOINTERFACE; end;