.NET a COM

Adam Boduch

Obiekty COM, w modelu programowania Win32 umożliwiały tworzenie komponentów wielokrotnego użytku. Raz napisane kontrolki mogły zostać użyte w każdym języku programowania obsługującym obiekty COM (czyli np. Delphi, C++ Builder, Visual C++). W .NET operacje te zostały uproszczone za sprawą wprowadzenia kodu pośredniego (IL) oraz specyficznej budowie podzespołów. Użytkownik zwolniony jest również z obowiązku rejestracji kontrolki (tak jak to było w przypadku COM), a współpraca pomiędzy poszczególnymi podzespołami jest prosta.

1 Terminologia COM
     1.1 Obiekt COM
     1.2 Interfejs COM
     1.3 GUID
     1.4 Serwer COM
     1.5 Biblioteki typu
2 Mechanizm COM Callable Wrappers
3 Przykładowy podzespół
4 Utworzenie biblioteki typu
5 Użycie biblioteki typu
6 Korzystanie z klasy COM
7 Kontrolki COM w aplikacjach .NET

Terminologia COM

Technologia COM jest zbyt ważna, aby teraz, wraz z nadejściem .NET, całkowicie z niej zrezygnować. Platforma .NET Framework posiada jednak możliwość używania kontrolek COM i udostępniania podzespołów aplikacjom COM. Nim ruszymy należy wprowadzić kilka pojęć związanych z technologią Component Object Model.

Obiekt COM

Obiekt COM jest fragmentem kodu binarnego, który wykonuje pewną funkcję. Obiekt COM ujawnia aplikacji pewne metody umożliwiające dostęp do jej funkcjonalności. Dostęp do tych metod odbywa się poprzez tzw. interfejsy COM.

W dalszej części tekstu słów obiekt i kontrolka będą używane zamiennie w rozumieniu obiektu COM.

Interfejs COM

Interfejs COM, w najprostszym rozumieniu pozwala na używanie obiektu COM, jest pośrednikiem pomiędzy aplikacją, a kodem znajdującym się w kontrolce. Z punktu widzenia Delphi interfejs to obiekt przypominający klasę (deklarowany z użyciem słowa kluczowego Interface).

Jedna kontrolka COM może zawierać jeden lub więcej interfejsów.

GUID

GUID to akronim od słów Global Unique Identifier. Jest to 128 bitowa liczba określający unikalny identyfikator kontrolki. GUID jest wykorzystywany m.in. przez system do identyfikacji kontrolki (kontrolka musi być zarejestrowana w systemie). Z naszego punktu widzenia GUID nie ma większego znaczenia. Wygenerowaniem odpowiedniego numeru zajmuje się Delphi, po użyciu kombinacji Ctrl+Shift+G.

Serwer COM

Mianem serwera COM określa się zwykły plik binarny (np. z rozszerzeniem *.dll), zarejestrowany w systemie. Serwer COM (czyli inaczej mówiąc ? kontrolka COM) zawiera wcześniej wspomniane interfejsy oraz klasy służące do "porozumiewania" się z klientem, czyli aplikacją, która korzysta z serwera COM.

Biblioteki typu

Biblioteka typu to specjalny plik zawierający informację o obiekcie COM. W skład tych informacji wchodzi lista właściwości, metod interfejsów. Biblioteki typu mogą być zawarte w obiekcie COM jako zasoby lub mogą stanowić oddzielny plik. Biblioteka typu jest niezbędna, aby klient mógł odczytać informację o kontrolce, o jej metodach i zwracanych typach. Biblioteka typu to plik posiadający rozszerzenie *.tlb.

Mechanizm COM Callable Wrappers

Ponieważ aplikacje Win32 są aplikacjami niezarządzanymi nie istnieje możliwość ich ingerencji w komponenty .NET. Chcąc uzyskać dostęp do komponentu .NET musimy użyć obiektu COM. Ponieważ jednak obiekty COM również należą do kodu niezarządzanego, skorzystamy z mechanizmu zwanego COM Callable Wrappers (w skrócie CCW). Tak więc dostęp do obiektu .NET będzie pośredniczony przez CCW; z tego względu będziemy musieli zarejestrować nasz podzespół, tak, aby mógł być użyty przez klientów COM.

dotnet_4.jpg

Podsumujmy: w Delphi 7 możemy użyć specjalnych funkcji, które utworzą serwer COM umożliwiający wykorzystanie kodu z podzespołu .NET.

Przykładowy podzespół

Nasza przykładowa aplikacja napisana w Delphi 8 będzie udostępniała interfejs szyfrujący tekst. Algorytm szyfrowania będzie prostą, aczkolwiek wydajną pętlą.

Nasza funkcja będzie umożliwiała zakodowanie wybranego tekstu przekazanego parametrem lpString. Kodowanie odbędzie się metodą tzw. xorowania, która jest metodą całkiem prostą w implementacji a do tego całkiem wydajną, dobrze nadaje się do szyfrowania prostych tekstów.

Kodowanie zwane potocznie xorowaniem swoją nazwę wzięło od operatora Xor, który umożliwia operowania na bitach. Wykorzystanie operatora Xor może wyglądać tak:

Result := 20 xor 5;

Systemy operują na systemie liczbowym ? same zera lub jedynki. W powyższym przykładzie liczba 20 może być liczbą do zakodowania, a 5 hasłem tak więc liczby te w postaci binarnej mogą wyglądać tak:

<tt>20 = 1110000
5 = 0001111</tt>

Nasza metoda szyfrowania polega na zestawieniu tych danych i porównaniu cyfr. Jeżeli dwie porównywane cyfry będą takie same (pierwsza cyfra to 0 i druga cyfra to 0) to wynikiem będzie cyfra 0 ? w przeciwnym wypadku ? cyfra 1.
Cała funkcja będzie wyglądała w ten sposób:

function TXorObject.Crypt(lpString, lpPassword: WideString): WideString;
var
  I : Integer;
  PassCount : Integer;
begin
  PassCount := 1;
  Result := lpString; // przypisz wartość początkową

  try

    for I := 1 to Length(lpString) do // wykonuj dla każdej litery osobno
    begin
      {
        Dla każdego osobnego znaku zamieniaj na wartość liczbowa, a następnie
        xoruj z każda litera hasła - powstaje wówczas unikalna kombinacja.
      }
      Result[i] := Chr(Ord(lpString[i]) xor Ord(lpPassword[PassCount]));
      Inc(PassCount);  // zwiększ licznik - kolejna litera hasła
      { Jeżeli licznik przekroczy długość hasła - wyzeruj }

      if PassCount > Length(lpPassword) then PassCount := 1;
    end;

  except
    raise Exception.Create('Błąd w trakcie szyfrowania');
  end;

end;

Kod jest w miarę prosty ? pewnie spodziewałeś się wielu linii kodu, a to wszystko zamykamy w jednej tylko funkcji. W powyższym przykładzie następuje kodowanie każdego znaku z osobna.

Na samym początku funkcją Ord przekształcamy znak do postaci cyfry, kodu ASCII. Tak samo robimy ze znakiem hasła poczym stosujemy na tych liczbach operator Xor. Liczbę zwróconą w rezultacie tego działania ponownie zamieniamy jako znak (Char) stosując funkcję Chr.

Nasz podzespół, w rezultacie udostępni taką klasę:

TXorObject = class(TObject)
  public
    function Crypt(lpString, lpPassword : String) : String;
    procedure About;
  end;

Pierwsza z metod (która została przedstawiona wcześniej) służy do szyfrowania; druga z nich ? wyświetla proste okienko dialogowe z informacją o autorze.

W naszym przykładzie skorzystamy z kompilatora Delphi 7. Wydaje mi się że jest to niezły przykład prezentujący w jaki sposób w kompilatorze Win32 można skorzystać z obiektu mieszczącego się w podzespole .NET. Przed użyciem naszego podzespołu musimy użyć specjalnego narzędzia (program, który dostarczany jest wraz z pakietem .NET Framework), który utworzy z naszego podzespołu bibliotekę typu.

Pełny kod źródłowy naszego podzespołu .NET znajduje się na listingu:

library XorAssembly;

{%DelphiDotNetAssemblyCompiler '$(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Windows.Forms.dll'}

uses
  System.Reflection,
  System.Runtime.InteropServices,
  System.Windows.Forms;

type
  IXorInterface = interface
  ['{47594C96-6A2E-464D-B64E-984203CE2FF4}']
    function Crypt(lpString, lpPassword : String) : String;
    procedure About;
  end;

  [ClassInterface(ClassInterfaceType.None)]
  TXorObject = class(TObject, IXorInterface)
  public
    function Crypt(lpString, lpPassword : String) : String;
    procedure About;
  end;

const
  AboutStr = 'Kontrolka Xor .NET ' + #13 + 'Copyright (c) 2004 by Adam Boduch';

{ TXorObject }

procedure TXorObject.About;
begin
{ wyświetla okienko pokazujące autora kontrolki }
  MessageBox.Show(AboutStr, 'O programie')
end;

function TXorObject.Crypt(lpString, lpPassword: WideString): WideString;
var
  I : Integer;
  PassCount : Integer;
begin
  PassCount := 1;
  Result := lpString; // przypisz wartość początkowa

  try

    for I := 1 to Length(lpString) do // wykonuj dla każdej litery osobno
    begin
      {
        Dla każdego osobnego znaku zamieniaj na wartość liczbowa, a następnie
        xoruj z każdą litera hasła - powstaje wówczas unikalna kombinacja.
      }
      Result[i] := Chr(Ord(lpString[i]) xor Ord(lpPassword[PassCount]));
      Inc(PassCount);  // zwiększ licznik - kolejna litera hasła
      { Jeżeli licznik przekroczy długość hasła - wyzeruj }

      if PassCount > Length(lpPassword) then PassCount := 1;
    end;

  except
    raise Exception.Create('Błąd w trakcie szyfrowania');
  end;
end;

begin

end.

Na samym początku, korzystając z dyrektywy DelphiDotNetAssemblyCompiler, włączamy do programu podzespół System.Windows.Forms.dll. Dzięki temu możemy w naszej aplikacji skorzystać z przestrzeni nazw System.Windows.Forms:

{%DelphiDotNetAssemblyCompiler '$(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Windows.Forms.dll'}

Kolejna istotna sprawa to interfejs. Interfejs udostępnia metody umożliwiające interakcję z obiektem COM. Interfejs posiada metody Crypt oraz About, które następnie są dziedziczone w klasie TXorObject:

TXorObject = class(TObject, IXorInterface)

Istotne jest to, że interfejsy nie posiadają implementacji metod. W istocie każde użycie metody z interfejsu, owocuje w rzeczywistości wywołaniem metody z klasy TXorObject.

W .NET możliwe jest zachowanie tradycyjnej konwencji zapisu GUID, czyli takiej jak na listingu. Innym sposobem jest skorzystanie z atrybutu GUID:

[Guid('47594C96-6A2E-464D-B64E-984203CE2FF4')]
IDotNetInterface = interface
...
end;

Utworzenie biblioteki typu

Jak wspomniałem wcześniej, utworzeniem biblioteki typu oraz jej rejestracją, zajmuje się program RegAsm.exe. Program należy wywołać z wiersza poleceń, z parametrem /tlb podając również ścieżkę do podzespołu:

<tt>C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322>regasm /tlb C:\x\XorAssembly.dll
Microsoft (R) .NET Framework Assembly Registration Utility 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Types registered successfully
Assembly exported to 'C:\x\XorAssembly.tlb', and the type library was registered successfully</tt>

Ostatni komunikat informuje, że podzespół został wyeksportowany do pliku C:\x\XorAssembly.tlb, a biblioteka typu została pomyślnie zarejestrowana. Oprócz tych komunikatów na konsoli mogą pojawić się ostrzeżenia związane z modułem Borland.Delphi.System, który jest automatycznie, mimowolnie włączany do pliku wykonywalnego. Ostrzeżenia są związane z niekompatybilnością w stosunku do Win32.

Użycie biblioteki typu

W celu skorzystania z biblioteki typu, musimy uruchomić Delphi 7. Owe środowisko zostało bowiem wzbogacone o polecenie Import Type Library, które umożliwia zaimportowanie biblioteki typu, w celu późniejszego wykorzystania naszego podzespołu.

Uruchom Delphi 7 i z menu Project wybierz Import Type Library, co spowoduje pojawienie się okna Import Type Library.

dotnet_5.jpg

W oknie, na liście odnajdź nazwę naszego podzespołu. W takim wypadku naturalnym stałoby się naciśnięcie klawisza Install, który spowodowałby zainstalowanie kontrolki. W tym jednak wypadku, przy nazwie klasy TClass pojawił się znak @, który nie jest dopuszczalny przez Delphi. Naciśnij więc przycisk Create Unit ? spowoduje on utworzenie modułu źródłowego dla naszej kontrolki, co może trochę potrwać.

To nie koniec. Ostatnim krokiem jest kompilacja tego modułu i dołączenie go do pakietu. Z menu File wybierz Open i odszukaj, a następnie otwórz z dysku plik dclusr.dpk (powinien znajdować się w katalogu Lib, w głównym katalogu z Delphi 7). W tym momencie będziemy musieli dodać plik źródłowy do pakietu, a następnie go skompilować.

dotnet_6.jpgg

W oknie zarządzania pakietami naciśnij przycisk Add, który umożliwia nam wybranie pliku źródłowego, który chcemy dodać. Plik źródłowy XorAssembly_TLB.pas powinien znajdować się w katalogu Imports, w głównym katalogu Delphi. W oknie dodawania nowego modułu naciśnij przycisk Browse i wskaż lokalizację pliku. Na końcu naciśnij OK, co spowoduje próbę kompilacji pliku źródłowego.

dotnet_7.jpg

Nie wiedzieć dlatego, Delphi wygenerował w pliku źródłowym XorAssembly_TLB.pas, następującą konstrukcję, która nie może być poprawnie skompilowana:

  TDateTime = packed record
    FValue: TDateTime;
  end;

W takich sytuacjach, kompilator wyświetli błąd: Type 'TDateTime' is not yet completely defined. Rozwiązaniem będzie doprowadzenie powyższego rekordu do takiej postaci:

  TDateTime = packed record
    FValue: DateTime;
  end;

To już ostatni krok. Teraz z menu Project możesz wybrać pozycję Build. Jeżeli wszystko pójdzie dobrze powinieneś zobaczyć okno takie jak na rysunku poniżej.

dotnet_8.jpg

W tym momencie obiekt COM został zarejestrowany w Delphi. Zamknij wszystkie okna (File | Close All) odpowiadając twierdząco na pytanie o zapisanie zmian.

Korzystanie z klasy COM

Nadszedł czas, aby wykorzystać nasz nowy komponent COM. W Delphi 7 otwórz nowy projekt i dodaj następujące moduły do listy Uses: XorAssembly_TLB, ComObj;

Główny moduł aplikacji wykorzystującej obiekt COM:

unit MainFrm;

{ KOMPILOWAĆ W DELPHI 7 }

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, XorAssembly_TLB, ComObj, StdCtrls;

type
  TMainForm = class(TForm)
    GroupBox1: TGroupBox;
    Label1: TLabel;
    memText: TMemo;
    Label2: TLabel;
    edtPassword: TEdit;
    btnCrypt: TButton;
    btnAbout: TButton;
    procedure btnCryptClick(Sender: TObject);
    procedure btnAboutClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  { zmienna wskazująca na obiekt }
    XorCrypt : IXorInterface;
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnCryptClick(Sender: TObject);
begin
{ jeżeli do zmiennej zostały przypisane jakieś dane -
  wykonaj kod. W tym przypadku wywołuje metodę Crypt.

  Tekst do zaszyfrowania oraz hasło pobierane jest z
  kontrolki TMemo raz TEdit. Zaszyfrowany tekst ponownie
  przekazywany jest do TMemo
}
  if Assigned(XorCrypt) then
  begin
    memText.Lines.Text := XorCrypt.Crypt(memText.Lines.Text, edtPassword.Text);
  end;
end;

procedure TMainForm.btnAboutClick(Sender: TObject);
begin
{
  jeżeli do zmiennej zostały przypisane jakieś dane -
  wykonaj kod. W tym przypadku wywołuje metodę About.
}
  if Assigned(XorCrypt) then
  begin
    XorCrypt.About;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ utwórz instancję obiektu COM }
  XorCrypt := CreateComObject(CLASS_TXOROBJECT) as IXorInterface;
end;

end.

Utworzenie obiektu COM, w programie z listingu odbywa się w zdarzeniu OnCreate formularza. Funkcja odpowiedzialna za utworzenie obiektu COM to CreateComObject. Parametrem tej funkcji jest tzw. CLASSID czyli unikalny identyfikator danej klasy. Generowanie CLASSID dla każdej klasy odbywa się automatycznie, więc my nie musimy się o to martwić. Działanie naszego przykładowego programu, prezentuje rysunek:

dotnet_9.jpg

Kontrolki COM w aplikacjach .NET

Wykonanie odwrotnej czynności ? tzn. import obiektu COM do aplikacji .NET ? jest o wiele prostszy. Obiekty Win32 COM mogą poddać się importowi do .NET przy pomocy narzędzia dołączonego do .NET Framework, o nazwie Tlbimp.exe.

Przykład importu obiektu COM do .NET oprzemy na kontrolce SAPI (Microsoft Speech API). Odszukaj na dysku pliku sapi.dll; z wiersza poleceń będziesz musiał uruchomić program Tlbimp.exe, który zaimportuje kontrolkę COM do .NET:

tlbimp "C:\Scieżka do pliku\sapi.dll" /verbose /out:C:\Interop.SAPI.dll

Takie użycie programu spowoduje utworzenie podzespołu Interop.SAPI.dll, który można w pełni wykorzystać w środowisku .NET. Normalnym jest, że podczas konwersji na konsoli wyświetlona zostanie masa komunikatów ostrzegających o możliwej niekompatybilności owej kontrolki z .NET.

Po utworzeniu podzespołu, skopiuj go do katalogu, w którym następnie utwórz nowy projekt WinForms. W pliku *.dpr projektu umieść odwołanie do podzespołu:

{%DelphiDotNetAssemblyCompiler 'Interop.SAPI.dll'}

Interfejs aplikacji będzie składał się z komponentu RichTextBox oraz Button. Po naciśnięciu przycisku, tekst z pola tekstowego, zostanie przekazany do biblioteki, co zaowocuje wywołaniem lektora, który przeczyta tekst. Rysunek prezentuje interfejs programu:

dotnet_10.jpg

Uruchomienie kodu znajdującego się w podzespole Interop.SAPI.dll wygląda tak:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Voice: SpVoice;
begin
  Voice := SpVoiceClass.Create;
  Voice.Speak(RichTextBox1.Text, SpeechVoiceSpeakFlags.SVSFDefault);
end;

Nie zapomnij o dołączeniu przestrzeni nazw na liście uses:

uses InterOp.Sapi;

0 komentarzy