Bob Swart (aka Dr.Bob)
Delphi 6 COM en Automation

Elke ontwikkelaar heeft van COM gehoord, alhoewel niet iedere ontwikkelaar er evenveel ervaring mee heeft. Voor sommige programmeurs is COM nog iets engs, alhoewel het dat absoluut niet is. Het is net als zwemmen: je moet even doorkomen, maar als je er eenmaal inzit dan is het hartstikke leuk. In dit artikel introduceer ik COM - Microsoft's Component Object Model - met behulp van Delphi. Ik zal COM Objecten en OLE Automation en behandelen, en een volgende keer verder gaan met MTS en COM+. Daarna zal ik de opgedane kennis en ervaring toepassen in een .NET platform (alhoewel Delphi daar tegen die tijd al wat meer mogelijkheden voor heeft).

COM Objects
Zoals ik in de introduktie al aangaf: COM staat voor Component Object Model, en met behulp van COM kunnen we objecten bouwen (en gebruiken) die draaien in een Windows omgeving, en volledige ontwikkelomgeving of programmeertaal onafhankelijk zijn. Ik kan dus in Delphi een COM object bouwen en dat in Visual Basic of C++ gebruiken (en andersom). In dit artikel zal ik mij beperken tot bouwen en gebruiken vanuit Delphi zelf - raadpleeg de documentatie behorende bij je eigen favoriete Windows ontwikkelomgeving voor de mogelijkheden tot het importeren en gebruiken van deze COM objecten.

Nieuw COM Object
Om met een eenvoudig voorbeeld te beginnen, gaan we een nieuw generiek COM Object maken met Delphi 6 (straks zal blijken dat er nog meer "speciale" COM mogelijkheden zijn, zoals een Automation Object, een Transactional Object of een Activer Server Object). Start Delphi 6, en sluit het default project (dat willen we niet gebruiken). Doe nu File | New - Other en ga naar de ActiveX tab van de Object Repository.

Hier staan de verschillende COM en ActiveX Wizards waarvan we er deze keer al een paar zullen gebruiken. We beginnen met een "normaal" COM Object, dus selecteer het icoontje en klik op OK. Dit geeft helaas een foutmelding, omdat COM Objecten alleen maar aan een project (executable of ActiveX DLL) toegevoegd kunnen worden. Dit worden dan resp. out-of-process (in een executable) en in-process (in een DLL) COM objecten overigens.

Laten we beginnen met een in-process server, dus doe nu weer File | New - Other, ga naar de ActiveX tab van de Object Repository en kies voor de ActiveX Library. Bewaar dit project in Euro42.dpr. Het bevat de volgende paar regels code:
  library Euro42;
  uses
    ComServ;

  exports
    DllGetClassObject,
    DllCanUnloadNow,
    DllRegisterServer,
    DllUnregisterServer;

  {$R *.RES}

  begin
  end.
Nu we de container voor onze COM Objecten hebben, kunnen we (voor de tweede keer) File | New - Other doen, en deze keer wel een COM Object toevoegen, wat resulteert in de volgende dialoog:

De Class Name zal gebruikt worden als interne en externe naam van het COM Object. Hierdoor is het niet aan te raden zomaar wat in te vullen, maar een beetje zinvolle naam, zoals "Euro" in dit voorbeeld. Het resultaat is een klasse TEuro, afgeleid van TTypedComObject, die het IEuro interface implementeert. De Instancing staat default al op Multiple Instance (wat wil zeggen dat meerdere clients tegelijkertijd met dezelfde server instantie kunnen werken), en het Threading Model staat op Apartment (zodat de verschillende instanties niet bij elkaars instance data kunnen - alleen globale variabelen zijn nog onvelig uiteraard). Zodra we de CoClass Name hebben ingevuld, zal automatisch de naam van het interface ook ingevuld zijn (in ons voorbeeld dus IEuro - afgeleid van IUnknown). Laat de checkbox "Include Type Library" aan staan, zodat we straks de Type Library Editor kunnen gebruiken om properties en methoden aan dit nieuwe interface toe te kennen. De laatste optie "mark interface Oleautomation" ga ik nu nog niet gebruiken, dus het maakt niet uit of die aan of uit staat op dit moment (ik zelf zet hem nu uit).

Na een druk op de OK knop zal de volgende unit gegenereerd worden (bewaar deze in unit Euro.pas):
  unit Euro;
  {$WARN SYMBOL_PLATFORM OFF}
  interface
  uses
    Windows, ActiveX, Classes, ComObj, Euro42_TLB, StdVcl;
  
  type
    TEuro = class(TTypedComObject, IEuro)
    protected
    end;
  
  implementation
  uses
    ComServ;
  
  initialization
    TTypedComObjectFactory.Create(ComServer, TEuro, Class_Euro,
      ciMultiInstance, tmApartment);
  end.

Type Library Editor
Behalve de nieuwe unit, krijgen we ook meteen de Type Library Editor te zien. Dit is de plek om nieuwe properties en methoden toe te voegen aan het COM Object - of specifieker gezegd, aan het IEuro interface. De Type Library kan gezien worden als de definitie van onze COM Objecten - letterlijk het interface met o.a. de properties en methoden, die met behulp van de Type Library Editor op eenvoudige wijze aan te passen en te onderhouden zijn. De Type Library zelf heeft de naam van het project en de extentie .tlb, en de import unit is Euro42_TLB.pas (dit bestand wordt iedere keer opnieuw gegenereerd nadat we een wijziging in de type library hebben gemaakt, dus het is over het algemeen niet zinvol om zelf wijzigingen in de type library import unit te maken).

We kunnen nu zelf nieuwe properties of methoden toevoegen aan het IEuro interface. Voor een nieuwe methode moeten we op de knop met de groene pijl drukken (tip: je kan de toolbar uittrekken waardoor de labels onder de knoppen komen - handig voor wie nog niet zovaak met de Type Library Editor heeft gewerkt). Laten we de methode EuroNaarGulden toevoegen, voor wie nog wat moeite heeft met de euro te rekenen en in zijn/haar hoofd(stiekem) alles nog in guldens doet. Ga op IEuro staan, druk op de New Method button, en noem de nieuwe methode dus EuroNaarGulden. Er moeten nog wel wat argumenten bijkomen, dus klik op de Parameters tab. Wat we daar aantreffen kan er op twee verschillende manieren uitzien: de "Pascal" manier of de "IDL" manier. De eerste laat alle types zien zoals Delphi ontwikkelaars die een beetje kennen (zoals WideString), terwijl de tweede juist de taal-onafhankelijke COM IDL (interface definition language) gebruikt - handig voor wie vaker met COM werkt. Ik heb zelf niet echt een voorkeur, en switch regelmatig heen en weer, wat je kan doen via Tools | Environment Options en dan de Type Library tab (de optie "language" kan op Pascal of IDL gezet worden). Laten we uitgaan van de setting op Pascal, dan moeten we twee parameters toevoegen, dus druk tweemaal op de Add knop. Noem de eerste parameter Euro en de tweede Gulden. Ze zijn allebei van type Double. Tot slot moeten we via de modifier aangeven of het om input, output of input/output parameters gaat. Default staat deze uit, wat input betekent. Echter, de Guldens parameter is een output parameter, en we moeten daarom de Modifier op "out" zetten:

Als je langere tijd met de Type Library Editor werkt kan het geen kwaad om aan het eind even op de witte knop met de twee kleine groene pijlen te klikken (de "refresh implementation" knop), die zorgt er namelijk voor dat de unit Euro.pas met daarin de TEuro klasse weer synchroon loopt met de wijzigingen die we in de type library hebben aangebracht. In dit geval hebben we een nieuwe functie die als volgt gedefinieerd is:
  type
    TEuro = class(TTypedComObject, IEuro)
    protected
      function EuroNaarGulden(Euro: Double; out Gulden: Double): HResult; stdcall;
    end;
Merk op dat ook het implementatie skelet er al staat (nog leeg, maar in tegenstelling tot event handlers blijven lege COM methoden gewoon staan gelukkig). De implementatie is uiteraard niet moeilijk:
  function TEuro.EuroNaarGulden(Euro: Double; out Gulden: Double): HResult;
  begin
    Gulden := Euro * 2.20371; // implementatie
  end;
Compileer nu het Euro42 ActiveX project, wat tot een warning leidt: de return waarde van de funktie EuroNaarGulden is niet gedefinieerd. Dat klopt, daar hebben we nog niks aan gedaan. Wie nog eens kijkt op Figuur 6 ziet dat daar als return type nog steeds de default type HResult staat. Waarom hebben we een funktieresultaat van type HResult eigenlijk nodig? Dat komt omdat het in de COM wereld een beetje gebruikelijk is om COM methoden een HResult waarde als mogelijke foutmelding terug te geven. Als HResult gelijk is aan S_OK dan is alles OK (dat lag voor de hand), en als HResult iets anders is, dan is er iets misgegaan. Omdat COM Objecten door vele verschillende omgevingen gebruikt kunnen worden, lijkt het me altijd beter om me maar aan de regels te houden, dus voeg ik één regel toe aan de implementatie van EuroNaarGulden, namelijk Result := S_OK
  function TEuro.EuroNaarGulden(Euro: Double; out Gulden: Double): HResult;
  begin
    Gulden := Euro * 2.20371; // implementatie
    Result := S_OK
  end;
Overigens begreep ik van Peter van Ooijen dat de Delphi compiler en runtime systeem zelf achter de schermen al van alles en nogwat doen met HResults. Zie Peter's artikel voor meer achtergrondinformatie hierover.

Nu kunnen we ons Euro42 project compileren, maar nog niet draaien, omdat het een DLL is en geen executable. COM Objecten worden gebruikt door zogenaamde COM Clients, en die gaan we nu bouwen.

Register COM Objects
Maar voordat we een COM Client gaan bouwen, moeten we eerst de COM Server gaan registreren. Dat kan met behulp van de Run | Register ActiveX Server menu optie. Hierdoor zijn meteen alle COM objecten in de Euro42.dll geregistreerd voor gebruik.

Als alternatief kun je regsvr32 aanroepen met de Euro42.dll als argument. Het extra argument /uninstall zorgt ervoor dat de COM Server weer verwijderd wordt.

COM Objecten gebruiken
Een COM Client kan zo'n beetje elk denkbaar Delphi project zijn, dus start maar een gewone nieuwe applicatie. Bewaar het main form in MainForm.pas en het project in EuroClient.dpr. We moeten nu zowel de ComObj unit als de Euro42_TLB.pas (type library import unit) aan de uses clause toevoegen, zodat het IEuro interface bekend is en gebruikt kan worden. Om het gebruik te demonstreren hebben we verder twee TEdits en een TButton nodig. Geef de TEdits de namen edtEuro en edtGulden, en de TButton de naam btnConvert. De code voor de OnClick event handler van de btnConvert is als volgt:

  procedure TForm1.Button1Click(Sender: TObject);
  var
    Euro: IEuro;
    Gulden: Double;
  begin
    Euro := CreateCOMObject(Class_Euro) as IEuro;
    Euro.EuroNaarGulden(StrToFloatDef(edtEuro.Text,0),Gulden);
    edtGulden.Text := Format('hfl. %.2f',[Gulden])
  end;
Als je dit compileert en uitvoert, dan zal de CreateCOMObject een instantie van onze COM Server genaamd "Class_ Euro" voor ons maken. De Type Library import unit bevat de definitie van de Class_Euro net als het IEuro interface zelf. Zodra de toepassing draait kunnen we 100 euro invullen en op de btnConvert button klikken voor het volgende resultaat:

Hoe zat dat nou met S_OK?
Eerder vertelde ik dat de EuroNaarGulden methode eigenlijk een funktie was die een HResult waarde teruggeeft - in ons geval altijd S_OK. Maar wat moeten we eigenlijk doen met deze HResult waarde? Welnu, het is "good practice" om iedere COM methode (die een HResult waarde teruggeeft) aan te roepen binnen een speciale wrapper genaamd OleCheck. Die controleert het resultaat, en als dat niet S_OK is krijgen we een foutmelding. Om toch maar netjes COM te gebruiken, zouden we dus de implementatie van de OnClick event handler als volgt kunnen herschrijven:

  procedure TForm1.Button1Click(Sender: TObject);
  var
    Euro: IEuro;
    Gulden: Double;
  begin
    Euro := CreateCOMObject(Class_Euro) as IEuro;
    OleCheck(Euro.EuroNaarGulden(StrToFloatDef(edtEuro.Text,0),Gulden));
    edtGulden.Text := Format('hfl. %.2f',[Gulden])
  end;
En als je echt netjes COM wil gebruiken moet je uiteraard ook een try-except blok om de CreateCOMObject heen zetten, voor het geval de COM Server niet gevonden kan worden op de client machine. Dat laat ik verder aan de lezer over, want zelf gaan we nu door met OLE Automation objecten.

Automation
Een Automation object is een speciaal COM object - het speciale zal straks duidelijk worden (nb: voor de meeste COM ontwikkelaars is een automation object het "gewone" scenario en een COM object het bijzondere geval - zie weer een van de artikelen van Peter van Ooijen voor details). We kunnen een nieuw automation object gewoon toevoegen aan onze bestaande Euro42 ActiveX Library (er kunnen meerdere COM objecten in dezelfde DLL opgenomen worden), met File | New - Other, en dan van de ActiveX tab het Automation Object icon. Dit geeft de volgende dialoog, die lijkt op die van een "normaal" COM Object:

We hoeven nu eigenlijk alleen maar weer de naam van de CoClass in te vullen, die ik op AutoEuro zet (we hebben Euro al gebruikt voor het normale COM Object). Instancing en Threading laat ik weer op hun default waarden staan, en events gebruik ik niet deze keer. Als we op OK klikken (bewaar de gegenereerde unit in AutoEuro.pas) komen we weer in de Type Library Editor terecht, die nu het interface bevat van zowel IEuro als IAutoEuro. En als je die twee met elkaar vergelijkt dan zie je dat IEuro van IUnknown is afgeleid, terwijl IAutoEuro van IDispatch is afgeleid. Het verschil zit hem in de "dispatch", wat zoveel betekent dat de methoden van het interface dynamisch opgezocht, toegewezen en uitgevoerd kunnen worden. IDispatch is op zijn beurt namelijk weer afgeleid van IUnknown, maar bevat een aantal extra methoden, namelijk GetTypeInfoCount, GetTypeInfo, GetIDsOfNames en Invoke. Hier kom ik straks nog even op terug. Ook de implementatie klassen verschillen van elkaar: TEuro is afgeleid van TTypedComObject, terwijl TAutoEuro afgeleid is van TAutoObject (die het IDispatch interface implementeert, dus we hoeven ons zelf niet druk te maken hierover).
Er is nog iets interessants te zien in de Euro42_TLB.pas unit, en dat is het feit dat er behalve de IAutoEuro interface definitie (afgeleid van IDispatch) ook een IAutoEuroDisp zogenaamd "dispinterface" aanwezig is, die dezelfde GUID heeft. Het effect hiervan zullen we straks zien, als we de AutoEuro op drie verschillende manieren kunnen gaan gebruiken.

Nieuwe methoden
Laten we eerst nog even twee methoden toevoegen. Dat gaat weer met behulp van de Type Library Editor net als met de vorige IEuro interface. Deze keer noem ik de methoden EuroToGuilder en GuilderToEuro, en geef ze wederom twee argumenten mee (het tweede argument weer van type "out"), die deze keer echter van type Currency zijn. De implementatie van EuroToGuilder en GuilderToEuro zal niet zo'n verrassing zijn denk ik:

  const
    GuilderPerEuro = 2.20371;

  procedure TAutoEuro.EuroToGuilder(Euro: Currency; out Guilder: Currency);
  begin
    Guilder := Euro * GuilderPerEuro
  end;

  procedure TAutoEuro.GuilderToEuro(Guilder: Currency; out Euro: Currency);
  begin
    Euro := Guilder / GuilderPerEuro
  end;
En hierna kunnen we het Euro42 project weer compileren en opnieuw registreren (zodat ook het AutoEuro automation object gevonden kan worden).

Automation Objecten gebruiken
En nu komen we bij een interessant deel van het artikel: waar we "normale" COM Objecten slechts op één manier kunnen gebruiken (met CreateCOMObject), daar kunnen we Automation Objecten op drie verschillende manieren gebruken, via variants (het meest flexibel, maar ook "gevaarlijkste"), via dispinterfaces, en tot slot via interfaces (de manier van de normale COM Objecten).

Variants (dispatch interfaces)
Waarom zouden we eigenlijk variants willen gebruiken? De belangrijkste reden is gemak. Variants maken gebruik van "late binding" om de methoden van het Automation Object aan te roepen. Ze hoeven dus niet tijdens compilatie al te weten welke methoden en argumenten beschikbaar (en geldig) zijn, maar zoeken dit tijdens het draaien wel uit. Helaas betekent dit ook dat er geen Code Insight ondersteuning meer is. Met alle gevolgen van dien als iemand een tikfoutje gemaakt heeft, want de methode EuroToGulden bestaat echt niet. Het voordeel is dat we geen Type Library import unit nodig hebben, en dat we ieder Automation Object op onze machine op deze manier kunnen benaderen (waaronder bijvoorbeeld de Office onderdelen), wat op z'n tijd best handig is. Voor het gebruik van de AutoEuro via Variants, is de code als volgt:

  procedure TForm1.Button1Click(Sender: TObject);
  var
    Euro: Variant;
    Guilder: Currency;
  begin

    Euro := CreateOleObject(EuroClassName)

    Euro.EuroToGuilder(StrToFloat(edtEuro.Text),Guilder); // GEEN Code Insight!!
    edtGuilder.Text := FloatToStr(Guilder)
  end;
Hoe werkt die Variant dan? Welnu, die gebruikt het IDispatch interface, die via de GetIDsOfNames een methode naam gebruikt om een dispatch ID op te halen, en vervolgens via het dispatch ID de daadwerkelijke methode aanroept. Dit zoeken kost tijd, en de Variants oplossing is dan ook de langzaamste van alle drie de oplossingen die we zullen zien.

Dispinterfaces
Een stapje "hoger" (en sneller) is het gebruik van dispinterfaces. Ook hierbij wordt late binding gebruikt. Echter, er is wel Code Insight ondersteuning aanwezig, dus we kunnen toch de aanwezige methoden en hun argumenten zien. Dat komt omdat het dispinterface alle methoden (en argumenten) definieert, zodat we hieruit kunnen kiezen. Op het moment dat de methoden worden aangeroepen, wordt via late binding het dispatch iD gebruikt om de juiste methode te vinden en aan te roepen. Er is geen GetIDsOfNames meer nodig (want het ID kennen we al), dus dit werkt niet alleen handiger (met Code Insight) dan de Variants oplossing, maar ook sneller. De code voor het gebruik van AutoEuro via dispinterfaces is als volgt:

  procedure TForm1.Button2Click(Sender: TObject);
  var
    Euro: IEuro42Disp;
    Guilder: Currency;
  begin
    Euro := CoEuro42.Create as IEuro42Disp;
    Euro.EuroToGuilder(StrToFloat(edtEuro.Text),Guilder); // WEL Code Insight!!
    edtGuilder.Text := FloatToStr(Guilder)
  end;
Hier is dus wel weer de Type Library import unit Euro42_TLB.pas voor nodig, overigens.

Interfaces
Het laatste voorbeeld maakt gebruik van de "normale" interfaces en early-binding zoals we die eerder zagen voor het IEuro object. De code voor het gebruik is hierbij als volgt:

  procedure TForm1.Button3Click(Sender: TObject);
  var
    Euro: IEuro42;
    Guilder: Currency;
  begin
    Euro := CoEuro42.Create;
    Euro.EuroToGuilder(StrToFloat(edtEuro.Text),Guilder); // WEL Code Insight!!
    edtGuilder.Text := FloatToStr(Guilder)
  end;
Het verschil tussen de drie methoden van gebruik zal duidelijk zijn. Soms is het noodzakelijk om variants te gebruiken, maar als het even kan pak ik de dispinterfaces. Los van het gebruik, echter, is het hopelijk ook duidelijk dat Automation Objecten (vanwege het feit dat ze op meerdere manieren gebruikt kunnen worden) te prefereren zijn boven normale COM Objecten. Ik bouw zelden meer een "normaal" COM Object, maar kies bijna altijd voor een Automation Object, zodat er potentieel meer clients mee kunnen praten. En hoe meer clients, hoe meer vreugde, nietwaar?
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .


Dit artikel is eerder verschenen in SDGN Magazine #72 - juni 2002
Met dank aan Peter van Ooijen voor de nodige verbeteringen en aanvullingen (t.o.v. het originele artikel in SDGN Magazine).

This webpage © 2002-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.