Wtyczki (Plugin) w oparciu o interfejsy

reichel

1 Wstęp
2 Co nieco o COM
3 Interfejs - wtyczka
4 Aplikacja - rodzic
5 Tworzenie wtyczki
6 Możliwości rozwoju
7 Implemenatacja interfejsu w innych językach
     7.1 MS Visual C++ 6.0
     7.2 MASM32
8 Wnioski
9 Bibliografia
10 Download

Wstęp

System wtyczek (potocznie zwanych pluginami) można zaprogramować na wiele sposobów. Począwszy od haków na okienka poprzez
biblioteki z zestawem funkcji czy też eksportujące tablice z zestawem funkcji skończywszy na rozwiązaniu, moim zdaniem
najbardziej uniwersalnym, a mianowicie na interfejsach. Wtyczka oparta o interfejs łączy zalety wszystkich pozostałych opcji a w istocie jest to tylko wygodne przedstawienie danych w trakcie tworzenia aplikacji gdyż po skompilowaniu zaimplementowany
interfejs można utożsamiać z rekordem zawierającym wskazniki do poszczególnych funkcji (tablice funkcji - VMTable, virtual method table).

Przedstawiony tutaj przykład jest podpatrzony z systemu windows i jego rozszerzeń. Wydaje mi się on najbardziej uniwersalny pod względem możliwości implementacji oraz jej łatwości w innych językach. Naturalnie można poszerzyć to rozwiązanie o biblioteki typów (TypeLib) czy też o komponenty w postaci kontrolek ActiveX (jednak w tym przypadku twórca wtyczki ma bardzo dużą kontrolę nad aplikacją i ciężko ją ograniczyć).

Co nieco o COM

Jak już wspomniałem idea wtyczek przedstawiona tutaj, jest oparta na wtyczkach systemu windows. Zatem stosować tu będę podobną konwencje. Każda z funkcji w interfejsie zwraca rezultat ( HResult ), który może przyjąć wartość S_OK w przypadku powodzenia oraz inne wartości w przypadku błędu (np. E_FAIL, E_NOTIMPL, ...) więcej w Windows.pas

Dzięki takiemu podejściu możemy wykorzystać funkcje już zdefiniowane w większości języków do sprawdzanie czy funkcja
zwróciła wartość poprawną. W delphi są to funkcje SUCCEEDED, OleCheck, FAILED.

{$EXTERNALSYM Succeeded}
function Succeeded(Res: HResult): Boolean;
{$EXTERNALSYM Failed}
function Failed(Res: HResult): Boolean;
{$EXTERNALSYM ResultCode}
function ResultCode(Res: HResult): Integer;
{$EXTERNALSYM ResultFacility}
function ResultFacility(Res: HResult): Integer;
{$EXTERNALSYM ResultSeverity}
function ResultSeverity(Res: HResult): Integer;
{$EXTERNALSYM MakeResult}
function MakeResult(Severity, Facility, Code: Integer): HResult;

Nie będę tutaj opisywał szczegółów związanych z posługiwaniem się interfejsami
odsyłam do książek [1][2].

Jedyne na co chciałbym zwrócić uwagę to na niszczenie interfejsow.
Rozpatrzmy interfejs IFoo oraz jego wersje zapakowaną razem z obiektem (TInterfacedObject)
TFoo.

 type
  IFoo = interface(IInterface)
  [SID_IFoo]
 end;
 type
  TFoo = class(TInterfacedObject, IFoo);
 end;

teraz dwa przypadki niszczenia, pierwsza z deklaracja zmiennej Foo jako interfejsu:

var
 Foo:IFoo;
begin
  Foo := TFoo.Create;
  //tu pracujemy z interfejsem
end;//na koniec funkcji zostanie on zniszczony

teraz drugi przypadek, w którym zmieniamy tylko deklaracje zmiennej Foo:

var
 Foo:TFoo;
begin
  Foo := TFoo.Create;
  //tu pracujemy z interfejsem
end;//na koniec funkcji !!!! nie !!!! zostanie on zniszczony

Interfejs - wtyczka

Projektując interfejs dla wtyczek powinniśmy się zastanowić jak dużą władze nad naszą aplikacją chcemy dać programistom (Lepiej jej dać za mało, bo łatwiej jest dodać uprawnienia niż je zabrać - kompatybilność).

Następnie musimy stworzyć opis takiego interfejsu jak np. przedstawiony tutaj (troche sztuczny).

//*************************************
//          Reichel Bartosz
//          reichel@mif.pg.gda.pl
//          http://reichel.pl
//          2006.08.19
//*************************************
unit MyInterface;

interface
uses Windows, ActiveX, COmObj;

//Scieżka w rejestrze, pod którą należy rejestrować
//nowe rozszerzenia. Należy utworzyć klucz z numerem
//GUID np:
//SOFTWARE\PFS\Examples\Wtyczki\{000C0AF6-5B6E-47BF-90F2-82E15480AB96}
//
const
 //HKEY_CURRENT_USER
 szWtyczki  = 'SOFTWARE\PFS\Examples\Wtyczki';


const
 SID_IPlugsApplication = '{822C0AF6-5B6E-47BF-90F2-82E15480AB96}';
 IID_IPlugsApplication: TGUID = '{822C0AF6-5B6E-47BF-90F2-82E15480AB96}';

//******************  IPlugsApplication ******************
//-----------
//function ChangeColor(NewColor:TColorRef):HResult;stdcall;
//
//funkca ChangeColor pozwala zmienić,
//kolor tła aplikacji rodzica. Jako parametr kolor w postaci RGB.
//-----------
//function GetHandle(out hwnd:HWND):HResult;stdcall;
//
//funkcja GetHandle zwraca uchwyt okna rodzica w parametrze hwnd
//*******************************************************
type
  IPlugsApplication = interface(IInterface)
  [SID_IPlugsApplication]
  function ChangeColor(NewColor:TColorRef):HResult;stdcall;
  function GetHandle(out hwnd:HWND):HResult;stdcall;
end;

const
 SID_IPlug = '{E2F26AD7-4A48-4CE4-8CC8-76192E0E9640}';
 IID_IPlug: TGUID = '{E2F26AD7-4A48-4CE4-8CC8-76192E0E9640}';

 SID_IPlugSuper = '{E9F1E678-C99B-46C2-A51F-AA4197182C73}';
 IID_IPlugSuper: TGUID = '{E9F1E678-C99B-46C2-A51F-AA4197182C73}';

//************************ IPlug ************************
//*******************************************************
//function Init(ap:IPlugsApplication;TabH:HWND): HResult; stdcall;
//
//W przypadku gdy implementujemy interfejs IPlugsApplication
//
//Funkcja Init jest najważniejszą częścią składową
//wtyczki opartej o IPlug i musi być zaimplementowana.
//Jako parametr należy podać istniejący wskaźnik do
//interfejsu aplikacji rodzica IPlugsApplication. W przeciwny
//wypadku funkcja zwróci wartość E_FAIL.
//
//Dla osób implementujących interfejs IPlug.
//
//Funkcja Init musi być zaimplementowana.
//W parametrze ap powinien znajdować się wskaźnik do
//interfejsu IPlugsApplication. Jeśli przyjmuje on
//wartość pustą funkcja Init powinna zwrócić kod
//E_FAIL i zakończyć inicjacje. W przypadku gdy wskaźnik
//jest poprawny, to należy w tej funkcji utworzyć okno
//wtyczki. W przypadku gdy parametr TabH jest różny od
//zera, okno może być utworzone z parametrem WS_CHILD.
//Jako wartość określającą powodzenie należy zwrócić S_OK
//---------------
//
// function Mouse(Name:PChar;X,Y:Integer): HResult; stdcall;
//
//Dla implementujących IPlugsApplication
//
//Do funkcji można przekazać parametry:
//      Name - opisujący nad czym znajduje się kursor
//      X,Y  - pozycje kursora myszy
//
//Dla implementujących interfejs IPlug
//
//Funkcja może być nie zaimplementowana i zwrócić E_NOIMPL.
//W przypadku gdy funkcja jest zaimplementowana
//można reagować na parametry Name, X,Y w zależności
//od zastosowania wtyczki.
//
//
//function GetName(var Desc:PChar):HResult; stdcall;
//
//Funkcja przekazuje dodatkowe informacje o wtyczcce.
//
//Funkcja GetName może być nie zaimplementowana i zwrócić
//wartość E_NOIMPL. W przypadku gdy jest zaimplementowana
//Powinna zarezerwować odpowiednią ilość pamięci dla zmiennej
//Name. Za zwolnienie pamięci odpowiedzialny jest wywołujący
//funkcje.
//
//
//function FreePlug(): HResult; stdcall;
//
//Funkcja FreePlug zwalnia zasoby pamięci zarezerwowane
//podczas działania wtyczki.
//
//Powinna zwrócić wartość S_OK.
//
//*******************************************************
//*******************************************************
type
  IPlug = interface(IInterface)
    [SID_IPlug]
    function Init(ap:IPlugsApplication;TabH:HWND): HResult; stdcall;
    function Mouse(Name:PChar;X,Y:Integer): HResult; stdcall;
    function GetName(var Desc:PChar):HResult; stdcall;
    function FreePlug(): HResult; stdcall;
  end;

//*********************** IPlugSuper ********************
//*******************************************************
//Interfejs IPlugSuper jest rozszerzeniem interfejsu
//IPlug. Implementujący ten interfejs powinien
//też zaimplementować funkcje interfejsu IPlug.
//
//
//function Super(): HResult; stdcall;
//
//Funkcja Super powinna zwrócić S_OK i zrobić coś Super!
//
//*******************************************************
//*******************************************************

type
  IPlugSuper = interface(IPlug)
    [SID_IPlugSuper]
    function Super(): HResult; stdcall;
  end;
implementation


end.

Dobrym zwyczajem jest podanie jakiegoś bardzo prostego przykładu wtyczki (coś w rodzaju SDK - software development kit, dla naszej wtyczki).

Należy szczególnie opisać jak należy reagować w przypadku błędu. Gdyż później nieprawidłowo napisane wtyczki mogą przyczynić się do niestabilności całej aplikacji.

Aplikacja - rodzic

Stworzenie dobrej aplikacji rodzica jest chyba najtrudniejszą sprawę. Programista piszący taką aplikację musi przewidzieć posunięcia przyszłych twórców wtyczek. Część związana z wywołaniem funkcji wtyczek powinna cechować się dobrą kontrolą błędów. Programista powinien przewidzieć nawet absurdalne posunięcia twórcy wtyczki i odpowiednio zareagować (np. usunąć wtyczkę z pamięci).

Chcielibyśmy jeszcze umożliwić komunikację wtyczki z aplikacją w obie strony. Dobrym do tego celu obiektem jest również interfejs, w tym przypadku przedstawiony jako IPlugsApplication. Pozwala on kontrolować kolor aplikacji oraz pobierać uchwyt okna głównego. Natomiast w drugą strone aplikacja rodzic wywołuje funkcje interfejsu IPlug lub IPlugSuper.

Definicja naszego formularza aplikacji rodzica powinna zatem zawierać w sobie deklaracje interfejsu, przedstawia to poniższy listing:

type
  TForm1 = class(TForm, IPlugsApplication)
    ListView1: TListView;
    OpenDialog1: TOpenDialog;

    .....

//IPlugsApplication
    function ChangeColor(NewColor:TColorRef):HResult;stdcall;
    function GetHandle(out hwnd:HWND):HResult;stdcall;
  public
    { Public declarations }
  end;

Natomiast funkcje (tu zmieniajaca kolor formularza i pobierająca uchwyt) powinny wyglądać w następujący sposób

function TForm1.ChangeColor(NewColor:TColorRef):HResult; begin
  Color := NewColor;
  result := S_OK;
end;

function TForm1.GetHandle(out hwnd:HWND):HResult; begin
 // hwnd := panel1.Handle;
  result := S_OK;
end;

Zgodnie ze wcześniejszym opisem interfejsu obie powinny zwrócić
S_OK.

Przejdźmy teraz do najważniejszej funkcji ListPlugs, wyświetla ona liste dostępnych wtyczek. Lista ta znajduje się w rejestrze pod ścieżką SOFTWARE\PFS\Examples\Wtyczki. Każda wtyczka reprezentowana jest jako numer CLSID (ten unikalny numer określający komponent COM, można wygenerować w Delphi poprzez wciśnięcie kombinacji klawiszy Ctrl+Shift+G).

Najbardziej istotną funkcją jest tu funkcja CoCreateInstance. Ładuje ona naszą wtyczkę do pamięci (w przypadku tutaj przedstawionym uruchamiana jest również funkcja Initialize w implementacji wtyczki). Jako parametry tej funkcji należy podać numer GUID (ang. Globally Unique Identifier). Po więcej szczegółów o funkcji COCreateInstance odsyłam do MSDN

HR := CoCreateInstance(WtykaGUID,nil,CLSCTX_INPROC_SERVER,IID_IPlug,W);

Jako interfejs za pomocą, którego będziemy się komunikowali z naszą wtyczką wybieramy GUID określający interfejs IPlug czyli IID_IPlug. Ten interfejs jest najbardziej podstawowy dla naszej wtyczki i każda wtyczka powinna posiadać jego implementacje.

Jeśli teraz chcemy przekonać się czy nasza wtyczka zawiera jakieś dodatkowe interfejsy, możemy wywołać funkcje QueryInterface z instancji utworzonej wtyczki. W naszym przypadku możemy oczekiwać, że wtyczka zawiera rozszerzenie w postaci interfejsu IPlugSuper. Zatem wywołujemy funkcje QueryInterface z parametrem IID_IPlugSuper. Jeśli funkcja zwróci rezultat S_OK, oznaczać to będzie, że wtyczka zawiera dodatkowy interfejs (a jego instancja
znajduje się w zmiennej SW:IPlugSuper).

W funkcji ListPlugs, kod niszczący obiekty jest wywoływany automatycznie.

procedure TForm1.ListPlugs;
var
 Reg:TRegistry;
 SL:TStringList;
 i:integer;
 WtykaGUID:TGUID;
 HR:HResult;
 PR:PPlugs;
 W:IPlug;
 Wname,Wpath:String;
 LI:TListItem;
 SW:IPlugSuper;
begin
   Screen.Cursor := crHourGlass;
   Button2.Enabled := False;
   ListView1.Items.BeginUpdate;
   try
    Reg := TRegistry.Create;
    try
     Reg.RootKey := HKEY_CURRENT_USER;
     Reg.OpenKey(szWtyczki,True);
     SL := TStringList.Create;
     try
      Reg.GetKeyNames(SL);
      if SL.Count > 0 then
       for i := 0 to SL.Count - 1 do
       begin

        if not CheckForPlug(SL[i]) then
        begin
        WtykaGUID := StringToGuid(SL[i]);
        HR := CoCreateInstance(WtykaGUID,nil,CLSCTX_INPROC_SERVER,IID_IPlug,W);
        if SUCCEEDED(HR) then
        begin
         GetInfoFromCLSID(WtykaGUID,Wname, Wpath);
         LI := ListView1.Items.Add;
         New(PR);
         PR^.Int := nil;//na razie interfejsu nie ma - jest niezaladowana
         PR^.Loaded := False;
         PR^.IsSuper := False;
         PR^.Path := WPath;
         PR^.Name := WName;
         PR^.CLSID := SL[i];
         LI.Data := PR;
         LI.Caption := IntToStr(i);
         LI.SubItems.Add(Wname);//nazwa
         LI.SubItems.Add(cStatus[False]);
         if SUCCEEDED(W.QueryInterface(IID_IPlugSuper,SW)) then
         begin
          LI.SubItems.Add('TAK');//Czy super wtyczka
          PR^.IsSuper := True;
         end
         else
          LI.SubItems.Add('NIE');

         LI.SubItems.Add(Wpath);//scieżka
         LI.SubItems.Add(SL[i]);   //CLSID
         W := nil;
        end;
        end;
      end;
     finally
      SL.Free;
     end;
    finally
     Reg.Free;
    end;
   finally
    ListView1.Items.EndUpdate;
    Button2.Enabled := True;
    Screen.Cursor := crDefault;
   end;
end;

Wciskając guzik uruchom, również wywołujemy funkcje CoCreateInstance, jednak tym razem wskaźnik do interfejsu przechowywujemy w rekordzie PPlugs. Wskaźnik do tego rekordu przechowywujem w liście ListView1. W procedurze LoadPlugClick, poza tym, że tworzymy instancje interfejsu to jeszcze wywołujemy funkcje Init. Jako parametry tej funkcji podajemy interfejs IPlugsApplication wskazujący na nasz formularz oraz uchwyt okna, na którym ma być utworzone okno naszej wtyczki (w przedstawionym przykładzie jest to uchwyt okna zakładki TTabSheet).

procedure TForm1.LoadPlugClick(Sender: TObject);
var
 LI:TListItem;
 PR:PPlugs;
 PlugGUID:TGUID;
 HR:HResult;

begin

if not Assigned(ListView1.Selected) then exit;
LI := ListView1.Selected;
PR := PPlugs(LI.Data);
PlugGUID := StringToGuid(PR^.CLSID);

HR := CoCreateInstance(PlugGUID,nil,CLSCTX_INPROC_SERVER,IID_IPlug,PR^.Int);
if SUCCEEDED(HR) then
begin
//Tworzenie nowej zakładki
      PR^.Tab := TTabSheet.Create(PageControl1);
      PR^.Tab.PageControl := PageControl1;
      PR^.Tab.Caption := PR^.Name;
      If SUCCEEDED(PR^.Int.Init(Form1 as IPlugsApplication, PR^.Tab.Handle)) then
        PR^.Loaded := true
      else
        PR^.Tab.Free;//W razie niepowodzenia należy ją usunąć.
end;

ListView1.SetFocus;
ListView1.Selected :=  LI;
ListView1.ItemFocused := LI;
ListView1Click(Sender);
end;

W związku z tym, że wskźniki do interfejsów są przechowywane w liście ListView1 jesteśmy zobowiązani zwolnić je sami. Niestety podczas destrukcji komponentów Delphi, nie zwalnia pamięci przydzielonej do pola Data poszczególnych pozycji w liście (TListItem).

Należy tu zwrócić uwagę na sprawdzanie warunków logicznych. Powinna być wyłączona dyrektywa B kompilatora (Boolean short-circuit evaluation). Gdyż sprawdzenie PR^.Loaded w przypadku gdy PR = nil, może spowodować błąd.

procedure TForm1.FormDestroy(Sender: TObject);
var
 I:Integer;
 PR:PPlugs;
begin
//W przypadku gdy interfejsy sa gdzieś 'zamotane' w pamięci
//nalezy je zwolnić samemu !!


if ListView1.Items.Count > 0 then
 for i:= 0 to ListView1.Items.Count -1 do
 begin
  PR := ListView1.Items[i].Data;
 //uwaga gdy włączone sprawdzanie wszystkich warunków to się posypię >> {$B-}
  if Assigned(PR) and PR^.Loaded then
  begin
    PR^.Int := nil; //zwalniamy wtyczke (to samo co _Release - nie pamietam, od ktorej wersji delphi tak mozna)
    PR^.Tab.Free;
  end;
  if Assigned(PR) then Dispose(PR);//no i zwalniamy pamięć przydzieloną do rekordow
 end;

   pMalloc := nil;
   CoUnInitialize;
end;

Wywołanie metody z interfejsu IPlugSuper dziedziczącego z IPlug, można rozwiązać za pomocą rzutowania lub za pomocą słowa kluczowego Delphi as

procedure TForm1.SuperClick(Sender: TObject);
var
 LI:TListItem;
 PR:PPlugs;
begin

 if not Assigned(ListView1.Selected) then exit;
 LI :=ListView1.Selected;
 PR := PPlugs(LI.Data);
 if not Assigned(PR) then exit;

 (PR^.Int as IPlugSuper).Super();

end;

Funkcja pobierająca nazwę jest podana jako przykład wywołania wyjątku za pomocą funkcji OleCheck. Efekt działania przedstawiony na rysunkach
Image8.pngImage7.png

procedure TForm1.Button1Click(Sender: TObject);
var
 LI:TListItem;
 PR:PPlugs;
 Name:PChar;
begin
if not Assigned(ListView1.Selected) then exit;
LI:=ListView1.Selected;
PR := PPlugs(LI.Data);
if Assigned(PR) and PR^.Loaded then
begin
 OleCheck(PR^.Int.GetName(Name));
 MessageBox(handle,name,'',MB_OK);
 pMalloc.Free(Name);
end;
end;

W przedstawionym tutaj przykładzie w funkcji OnMouseMove, listowane są wszystkie wtyczki i wysyłana jest do nich informacja, poprzez funkcje Mouse(name,X,Y), o położeniu kursora myszy oraz o nazwie komponentu nad jakim znajduje się kursor.

procedure TForm1.Button1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
 I:Integer;
 PR:PPlugs;
 SC:String;
begin

if ListView1.Items.Count > 0 then
 for i:= 0 to ListView1.Items.Count -1 do
 begin
  PR := ListView1.Items[i].Data;
  if Assigned(PR) and PR^.Loaded then
  begin
   if Assigned(Sender) then
   begin
    SC := Sender.ClassName;
    if assigned(PR^.Int) then PR^.Int.Mouse(Pchar(SC),X,Y);
   end;
  end
 end;

end;

Funkcja RegisterInterfaces rejestruje nazwy interfejsów w rejestrze. W takim przypadku inne programy mając numer CLSID, mogą przedstawić nazwę interfejsu w bardziej czytelnej formie (Patrz program OleView) [3].

procedure RegisterInterfaces;
var
 Reg:TRegistry;
begin
  Reg := TRegistry.Create;
  try
   Reg.RootKey := HKEY_CLASSES_ROOT;
   Reg.OpenKey('Interface\'+SID_IPlugsApplication,True);
   Reg.WriteString('','IPlugsApplication');
   Reg.CloseKey;

   Reg.OpenKey('Interface\'+SID_IPlug,True);
   Reg.WriteString('','IPlug');
   Reg.CloseKey;

   Reg.OpenKey('Interface\'+SID_IPlugSuper,True);
   Reg.WriteString('','IPlugSuper');
   Reg.CloseKey;
  finally
   Reg.free;
  end;
end;

Aplikacja rodzic w akcji:
Image2.png

Tworzenie wtyczki

Aby utworzyć wtyczke w postaci biblioteki dll, trzeba zaimplementować interfejs IPlug. Można tego dokonać w Delphi, dobudowywując go do specjalnie do tego przeznaczonego obiektu TComObject.

type
  TDelphiWtyczka = class(TComObject,IPlug)
  private
    FWind: TForm1;
  public
//IPlug
    function Init(ap:IPlugsApplication;TabH:HWND): HResult; stdcall;
    function Mouse(Name:PChar;X,Y:Integer): HResult; stdcall;
    function GetName(var Desc:PChar):HResult; stdcall;
    function FreePlug(): HResult; stdcall;
 public
    procedure  Initialize; override;
    destructor Destroy; override;
  end;

const

  CLSID_DelphiWtyczka: TGUID = '{A0D2EB0D-BF63-4C77-AE0D-89B0234711C9}';

Obiekt ten posiada dodatkowo jeszcze funkcję wywoływaną podczas inicjacji - Initialize. W destruktorze powinniśmy usunąć wszelkie obiekty z pamięci.

procedure TDelphiWtyczka.Initialize;
begin
//Tutaj incjujemy elementy potrzebne do działania naszego pluginu
  inherited;
end;

destructor TDelphiWtyczka.Destroy;
begin
  if Fwind <> nil then FWind.Close;
  inherited;
end;

W funkcji Init, która musi być zaimplementowana do poprawnego działania naszej wtyczki, powinniśmy utworzyć okno i umieścić je na oknie rodzicu (uchwyt podany w parametrze TabH). Naturalnie w naszych wtyczkach nie jesteśmy zmuszeni tworzyć okna, możemy np. pobierać uchwyt kontekstu okna rodzica i malować po nim za pomocą dostępnych w danym języku funkcji (lub też bezpośrednio wykorzystywać funkcje WinAPI z modułu GDI).

function TDelphiWtyczka.Init(ap:IPlugsApplication;TabH:HWND): HResult;
begin
//zakładamy, że aplikacja musi mieć interfejs aplikacji.
   if ap = nil then
   begin
     result := E_FAIL;
     Exit;
   end;

   Fwind := TForm1.CreateP(TabH);
   Fwind.top := 10;
   Fwind.left := 10;
   Fwind.show;
   result := S_OK;
end;

Przedstawiony przypadek jest minimalistyczny, więc w funkcji Mouse zwracamy E_NOTIMPL.

function TDelphiWtyczka.Mouse(Name:PChar;X,Y:Integer): HResult;
begin
// Jeśli zwrócimy, ze metoda jest niezaimplementowana
//aplikacja rodzic może zareagować na to w odpowiedni sposób.
//W najgorszym przypadku np stosując funkcje OleCheck
//może zwrócić wyjątek, ze metoda jest niezaimplementowna.
 result := E_NOTIMPL;
end;

Podobnie czynimy z funkcją GetName. Natomiast w funkcji FreePlug zamykamy okno.

function TDelphiWtyczka.GetName(var Desc:Pchar):HResult;
begin
 result := E_NOTIMPL;
end;

function TDelphiWtyczka.FreePlug(): HResult;
begin
  FWind.Close;
  Result := S_OK;
end;

Aby naszą bibliotekę można było zarejestrować powinna ona eksportowaćzestaw funkcji specyficznych dla interfejsów COM.

exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

Dodatkowo powinna zawierać tzw. Fabrykę Klas (ClassFactory związane z interfejsem IClassFactory). W Delphi (i w większości większych kompilatorów) jest już zaimplementowana klasa ClassFactory. My możemy ją rozbudować o dodatkową rejestracje naszej biblioteki w rejestrze dla potrzeb aplikacji rodzica.

type
  TPFSPlugFactory = class(TComObjectFactory)
  public
    procedure UpdateRegistry(Register: Boolean); override;
  end;

procedure TPFSPlugFactory.UpdateRegistry(Register: Boolean);
var
  ClassID: string;
begin
  if Register then begin
     inherited UpdateRegistry(Register);

      ClassID := GUIDToString(CLSID_DelphiWtyczka);
      with TRegistry.Create do
        try
          RootKey := HKEY_CURRENT_USER;
          OpenKey(szWtyczki+ClassID,True);
          CloseKey;
        finally
           Free;
        end;
  end
  else begin
       with TRegistry.Create do
        try
          RootKey := HKEY_CURRENT_USER;
              ClassID := GUIDToString(CLSID_DelphiWtyczka);
          DeleteKey(szWtyczki+'\'+ClassID);
        finally
           Free;
        end;
    inherited UpdateRegistry(Register);
  end;
end;

initialization
CoInitialize(nil);
  TPFSPlugFactory.Create(ComServer, TDelphiWtyczka, CLSID_DelphiWtyczka, '',
      'Wtyczka Delphi', ciMultiInstance, tmApartment);
finalization
CoUnInitialize;
end.

Nasza wtyczka po rejestracji w programie OleView powinna wyglądać jak na rysunku

Image9.png

Aby teraz dodać naszą wtyczkę, wystarczy wybrać opcje Zarejestruj z aplikacji rodzica lub też wydać polecenie z linii komend regsvr32 nasza_wtyczka.dll.

Możliwości rozwoju

Interfejsy dają niesamowicie łatwą możliwość rozwoju wtyczek. Dodatkowo pozwalają one być kompatybilne wstecz. Jako przykład dołączam drugą wtyczkę, zawierającą dodatkowo implementacje interfejsu IPlugSuper. Interfejs IPlugSuper dziedziczy wszystkie funkcje z Interfejsu IPlug oraz IUnknown (oraz ISupportErrorSupport) dodatkowo dodając funkcje Super.

type
  TDelphiSuperWtyczka = class(TComObject,IPlug, IPlugSuper)
  private
   pMalloc: IMalloc;
   Fwind: TSuperForma;
  public
//IPlug
    function Init(ap:IPlugsApplication;Tab:HWND): HResult; stdcall;
    function Mouse(Name:PChar;X,Y:Integer): HResult; stdcall;
    function GetName(var Desc:PChar):HResult; stdcall;
    function FreePlug(): HResult; stdcall;
//IPlugSuper
    function Super(): HResult; stdcall;
 public
    procedure  Initialize; override;
    destructor Destroy; override;
  end;

const

  CLSID_DelphiSuperWtyczka: TGUID = '{0A3963BD-DC55-4BC7-BBC6-C5771891F990}';

Banalna implementacja funkcji super.

  function TDelphiSuperWtyczka.Super(): HResult;
begin
  MessageBox(GetFocus,PChar('Witaj, jestem Super Wtyczka'),'',0);
  Result := S_OK;
end;

W drugiej wtyczce przedstawiam sposób w jaki można posługiwać się interfejsem IMalloc do zarządzania pamięcią.

function TDelphiSuperWtyczka.GetName(var Desc:PChar):HResult;
const
  desz  =  'Super wtyczka delphi';
begin
 Desc := pMalloc.Alloc(Length(desz)+1);
 StrCopy(PChar(Desc),desz);
 //rezerwujemy pamięć,
 //aplikacja rodzic jest odpowiedzialna za jej zwolnienie
 Result := S_OK;
end;

Nasza SUPER wtyczka po rejestracji w programie OleView powinna wyglądać jak na rysunku

Image10.png

Implemenatacja interfejsu w innych językach

MS Visual C++ 6.0

Wpierw należy zdefiniować interfejs w c++

#ifndef _MYINTERFACE_H
#define _MYINTERFACE_H



#undef  INTERFACE

#define INTERFACE IPlugsApplication
DECLARE_INTERFACE_(IPlugsApplication, IUnknown)
{
    STDMETHOD(QueryInterface)(THIS_ REFIID,PVOID*) PURE;
	STDMETHOD_(ULONG,AddRef)(THIS) PURE;
	STDMETHOD_(ULONG,Release)(THIS) PURE;
	STDMETHOD(ChangeColor)(THIS_ LONG) PURE;                                      
	STDMETHOD(GetHandle)(THIS_ HWND) PURE;
};
typedef IPlugsApplication *    LPPLUGSAPPLICATION;

#undef  INTERFACE

DEFINE_GUID(IID_IPlug,  0xE2F26AD7, 0x4A48, 0x4CE4, 0x8C, 0xC8, 0x76, 0x19, 0x2E, 0x0E, 0x96, 0x40);

#define INTERFACE IPlug
DECLARE_INTERFACE_(IPlug, IUnknown)
{
    STDMETHOD(QueryInterface)(THIS_ REFIID,PVOID*) PURE;
	STDMETHOD_(ULONG,AddRef)(THIS) PURE;
	STDMETHOD_(ULONG,Release)(THIS) PURE;
    STDMETHOD_(ULONG,Init)(THIS_ struct IPlugsApplication *, HWND) PURE;
    STDMETHOD(Mouse)(THIS_ char* name, int X, int Y) PURE;
    STDMETHOD(GetName)(THIS_ char* Desc)PURE;
    STDMETHOD(FreePlug)(THIS) PURE;
};
typedef IPlug *    LPPLUG;

#undef  INTERFACE



#endif

Wtyczkę budujemy jako obiekt ATL

Zatem wybieramy nowy projekt (ATL COM AppWizard)
at1.png

Następnie dodajemy nowy obiekt (Simple Object)
at2.png

i nadajemy mu nazwę
at3.png

Teraz należy tylko zaimplementować interfejs i IPlug i dodać
odpowiednie wpisy do pliku *.rgs

HKCR
{
	NewIntf.VCPlug.1 = s 'VCPlug Class'
	{
		CLSID = s '{D80E7000-A912-4DA1-9322-0CC21D506CEF}'
	}
	NewIntf.VCPlug = s 'VCPlug Class'
	{
		CLSID = s '{D80E7000-A912-4DA1-9322-0CC21D506CEF}'
		CurVer = s 'NewIntf.VCPlug.1'
	}
	NoRemove CLSID
	{
		ForceRemove {D80E7000-A912-4DA1-9322-0CC21D506CEF} = s 'Wtyczka VC++'
		{
			ProgID = s 'NewIntf.VCPlug.1'
			VersionIndependentProgID = s 'NewIntf.VCPlug'
			ForceRemove 'Programmable'
			InprocServer32 = s '%MODULE%'
			{
				val ThreadingModel = s 'Apartment'
			}
			'TypeLib' = s '{F84EBD92-0BA1-45EF-A5C8-A40B69538E4C}'
		}
	}
}

HKCU
{
  NoRemove Software
  {
    NoRemove PFS
    {
     NoRemove Examples
     {
      NoRemove Wtyczki
      {
        ForceRemove  {D80E7000-A912-4DA1-9322-0CC21D506CEF} = s 'Wtyczka VC++' 
      }   
     }
    }
  }
}

nasza wtyczka powinna działać
wtvcpp.png

MASM32

Wtyczka napisana w asemblerze, jest dzisiaj raczej czymś co można traktować jedynie w kategoriach edukacyjnych. ten przykład pokazuje, że wtyczkę opartą na interfejsach można stworzyć niemalże w każdym języku programowania. Dodatkowym bardzo ważnym aspektem, jest możliwość poznania działania interfejsu od podstaw. Zachęcam do przejrzenia źródeł, w szczególności do implementacji interfejsu IClassFactory. Kod tu przedstawiony jest oparty o szablony dołączone do paczki zawierającej kompilator MASM32 7 dostępny na stronie Iczelion'a [4].

Z ciekawostek należy zauważyć, że rozmiar wtyczki to około 17 kb przy czym sama bitmapa zajmuje około 11 kb. Cała wtyczka skompresowana UPX'em zajmuje około 5kb. Niestety napisanie bardziej skomplikowanej wtyczki w asemblerze jest nieopłacalne w przypadku usług komercyjnych (no chyba, że ktoś za to płaci).

Przedstawię tu tylko fragmenty najbardziej istotne z punktu widzenia twórcy wtyczki. Najważniejszą częścią jest deklaracja interfejsów. Interfejs IPlug będziemy implementować, tak więc potrzebna jest nam jego definicja(właściwie definicja tablic wirtualnych funkcji - _vtIASMPlug), oraz struktura, w której przechowamy nasz obiekt - ASMPlugObject.

Interfejs IPlugsApplication nie jest implementowany we wtyczcce, zatem wystarczy jego deklaracja na potrzeby funkcji coinvoke. W tym przypadku bardzo ważne jest poprawne określenie ilości parametrów przekazywanych do funkcji. Dokonuje się tego przy pomocy słowa kluczowego comethod, na końcu podając liczbę parametrów. Należy pamiętać, że w językach wysokiego poziomu (obsługującego obiekty), pierwszym parametrem jest wskaźnik do samego obiektu. Z tego też powodu procedury nie posiadające normalnie (Delphi, VC++) parametrów, takie jak AddRef, Realease tu posiadają jeden parametr.

;Definicja wirtualnych tablic funkcji dla naszej przyszłej
implementacji ;interfejsu IPlug

_vtIASMPlug MACRO CastName:REQ
    ; metody interfejsu IUnknown
    _vtIUnknown CastName
    ;metody implementacji interfejsu IPlug tu implementacja nazwana IASMPlug
    &CastName&_Init     comethod3   ?
    &CastName&_Mouse    comethod4   ?
    &CastName&_GetName  comethod2   ?
    &CastName&_FreePlug comethod1   ?
ENDM



IASMPlug                   STRUCT
    _vtIASMPlug IASMPlug
IASMPlug                   ENDS


; Deklaracja interfejsu zawiązanego z aplikacja główna ;
IPlugsApplication Interface IPlugsApplication            STRUCT
DWORD
       IPlugsApplication_QueryInterface       comethod3       ?
       IPlugsApplication_AddRef               comethod1       ?
       IPlugsApplication_Release              comethod1       ?
       IPlugsApplication_ChangeColor          comethod2       ?
       IIPlugsApplication_GetHandle           comethod2       ?
IPlugsApplication           ENDS

; Deklaracja obiektu ASMPlug ASMPlugObject         STRUCT   ;
this_
    ; interface object
    lpVtbl          DWORD       0
    ; object data
    nRefCount       DWORD       1
    hWnd          HWND 0 ;uchwyt okna aplikacji - potrzebny do zniszczenia
    hBmp          DWORD 0 ;uchwyt bitmapy
    APPInt    DWORD  ? ;Wskaźnik do interfejsu IPlugsApplication
ASMPlugObject         ENDS

Wtyk asm i inne:

asmwtyk.png

Podczas tworzenia interfejsu powinniśmy zapisać uchwyt bitmapy oraz uchwyt utworzonego okna w celu ich późniejszego zniszczenia. Tu właśnie skorzystamy z pierwszego parametru przekazywanego do wszystkich funkcji i wskazującego na sam obiekt (this_). Zapisujemy też wskaźnik do interfejsu aplikacji rodzica w polu APPInt naszej implementacji interfejsu IPlug.

Procedura inicjująca wtyczkę

Init proc this_:DWORD, IPA:DWORD, TABH:HWND

;robimy kopie wskaźnika do interfejsu aplikacji wtyczek
    mov edx, this_
    mov eax, IPA
    mov (ASMPlugObject ptr [edx]).APPInt,eax

;ładujemy bitmape, oraz okno i przypisujemy uchwyty ;do zmiennych
w obiekcie COM

    invoke LoadBitmap,g_hDllMain,100

    mov edx, this_
    mov AsmPlug, edx
    mov (ASMPlugObject ptr [edx]).hBmp, eax


    invoke WinMain, g_hDllMain, TABH

    mov edx, this_
    mov (ASMPlugObject ptr [edx]).hWnd, eax



    xor eax, eax        ; return S_OK
    ret
Init endp

Podczas wciskania guzika Zmień kolor, do okna wysyłany jest komunikat WM_COMMAND z parametrem wskazującym na dany guzik (w tym przypadku numer 501). Powinniśmy teraz odzyskać wskaźnik do interfejsu IPlugsApplication. W przedstawionym przypadku nie jest to rozwiązanie dobre dla wtyczek wielowątkowych, jednak z założenia miała być ona jednowątkowa. Do wywoływania funkcji związanych ze wskaźnikiem do interfejsu, służy makro coinvoke.

Zdarzenie "wciśnięcie guzika - zmień kolor"

     .elseif wParam == 501
           mov edx, AsmPlug
           mov eax, (ASMPlugObject ptr [edx]).APPInt
; makro cinvoke służy do wywoływania procedur związanych z
interfejsami COM ; w tym przypadku zmieniamy kolor aplikacji
głównej na czerwony (255)
           coinvoke eax, IPlugsApplication, ChangeColor, 255
           xor eax,eax
           ret
      .endif

Jest to dobre miejsce, w którym możemy uruchomić program OLEView i przypatrzeć się co też nasze wtyczki tak naprawdę implementują. Obrazek pokazuje trzy wtyczki napisane w VC++., Delphi oraz assemblerze. W każdej z nich (MY) implementowaliśmy tylko interfejs IPlug oraz IUnknown. Tymczasem w przypadku Delphi możemy dostrzec jeszcze dodatkowy interfejs ISupportErrorInfo. Jego implementacja związana jest z
obsługą błędów dla interfejsów w Delphi (a dokładniej obiekt TComObject implementuje ten interfejs do obsługi błędów).

W VC++ także istnieje możliwość automatycznej implementacji interfejsu ISupportErrorInfo podczas tworzenia obiektu ATL. Dodatkowo w VC++ tworzony jest interfejs związany z naszą wtyczką IVCPlug. Interfejs IDispatch służy do komunikowania się z interfejsem w przypadku gdy aplikacja nie zna metod interfejsu (interfejs jest dowiązany w trakcie działania programu a nie podczas kompilacji).

Interfejsy implementowane przez różne wtyczki
3wtyk_ole.png

Wnioski

Przedstawiony przykład pokazuje jak napisać aplikację z możliwością dodawania wtyczek, w przypadku Delphi. Jednak już sama możliwość implementacji wtyczek w innych językach pokazuje, że idea stosowania interfejsów jest na tyle uniwersalna, że bez problemu aplikacja rodzic jak i wtyczki mogą być tworzone w dowolnym języku (kompilatorze) pozwalającym obsłużyć interfejsy (C++, Delphi, FreePascal, C#, Assembler, Visual Basic, ...). Dodatkowo idea zapożyczona z implementacji interfejsów powłoki systemu Windows, pozwala w łatwy sposób bazować na dostępnych przykładach np. IContextMenu.

Bibliografia

[1] K. Brockschmidt. Inside OLE 2 (2nd ed.). Microsoft Press, Redmond, WA, USA, 1995.
[2] X.Pacheco and S.Teixeira. Delphi 6 Vademecum profesjonalisty Tom II. Helion (Polish edition),2002.
[3] Windows 2000 resource kit tool : Ole/com object viewer (oleview.exe). Microsoft.
http://www.microsoft.com/downloads/details.aspx?FamilyID=5233b70d-d9b2-4cb5-aeb6-45664be858b6&DisplayLang=en.
[4] Iczelion?s win32 assembly homepage. http://win32asm.cjb.net/.

Download

Aktualne pliki:
http://rudy.mif.pg.gda.pl/~reichel/down.php?id=236

pliki stare.....
Plik z kodem:
kodwt.zip
Pliki z kodem wtyczki (vc++):
NewIntf.zip
Tekst w PDF:
wtyczki_intf.zip
Binarki:
Project1.zip
super.zip
wtyka_delphi.zip

4 komentarzy

Znalazlem kilka drobnych bledow w tekscie, i kodzie np brak +'' powodujacy bledna odrejestracje. Ale za nic nie moge podmienic plikow w downloadzie ?!

Jak to sie robi ... :)

ps. nikt nie kaze pisac wtyczek w delphi che che. W innych spodziewam sie, ze zajma kilka-kilkadziesiat kilobajtow. A jesli juz czlowiek decyduje sie na aplikacje z wtykami pisanymi w delphi moze dolaczyc opis ze np plik vcl50.bpl jest dolaczany do programu.

niezły art, gratulacje :-)

Kapitalny Art, dzieki wielkie !!!

Szkoda tylko że wtyczka jako DLL zajmuje tyle co sam program, ale artykuł super - z pewnością przeczyta go wiele osób.

Z kolei ja też zrobiłem system wtyczek na interfejsach, ale w oparciu o pliki BPL, wtyczka zajmuje wtedy kilkadziesiąt kB zamiast kilkuset, tylko nie wiem czy można wtedy pisać wtyczki w innych językach.