Хотел я в общих словах расказать основную идею С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;
Простыми словами о Com
Автор
daimond
, ��� 15 2008 05:08 , В этой теме нет ответов