COM komponensekről röviden
A COM komponensek olyan önálló egységek, melyek bináris formában jelennek meg, rendszerint egy DLL vagy egy EXE fájl formájában.
Egy komponens tulajdonképpen több úgynevezett COCLASS (COM osztály) összefogása. Egy COCLASS több interfészt is megvalósíthat. Ha egy COCLASS valamilyen interfészt megvalósít, akkor az azt jelenti, hogy a COCLASS valamilyen képességgel rendelkezik. Egy COCLASS nagyon hasonló az objektum-orientált szemléletmódban megszokott osztályhoz.
Az interfészekhez metódusokat definiálhatunk, melyek valamilyen működésre lesznek majd képesek. Az interfész csak egy definíció, melyhez az összes, az interfészt megvalósító osztálynak ragaszkodnia kell. Az interfész csak meghatározza, hogy minek kell történnie, azonban ennek megvalósítását az osztályra bízza.
Az interfészek, a COCLASS azonosítására egy 128 bites szám szolgál (GUID, Globally Unique IDentifyer). Egyedi azonosító, melynek generálása annyira sok összetevőn múlik, hogy szinte kizárt egy azonosító két objektumhoz rendelése.
A COM interfészeket valamilyen programozási nyelvtől független módon kell definiálni, hogy a komponensek készítésének és használatának programozási nyelve független tudjon lenni. Ennek egyik módja az IDL (Interface Definition Language). Ha az interfész IDL nyelven van megfogalmazva, akkor ezt követően egy IDL fordítóval tudjuk azt a többi programnyelv számára érthető formába konvertálni.
Egy másik módszer, hogy valamilyen szabványos, bináris formában tároljuk az interfésszel kapcsolatos információkat. Ez COM esetén a TYPE LIBRARY, vagy típuskönyvtár, melyet vagy egy különálló TLB kiterjesztésű állomány tartalmaz, vagy maga a komponens.
A COM komponensek alapvető interfésze az IUnknown, ezt minden komponensnek biztosítania kell. Az IUnknown interfész lehetővé teszi, hogy a COM-kliens mutatókat kapjon a komponens egyéb használható interfészeire. Annak érdekében, hogy lekérdezzük, milyen interfészek állnak rendelkezésre, a QueryInterface metódus szolgál. Tulajdonképpen a komponensek valamennyi interfésze ebből az alap interfészből származik közvetlenül, vagy közvetve.
Az interfész másik két metódusa az AddRef és a Release. A kliensnek minden interfészpointerre legelőször az AddRef metódust kell meghívnia, majd amikor nem használ egy adott interfészpointert, meg kell hívnia a Release metódust. Az AddRef metódussal emeljük az interfészre mutató hivatkozások számát, a Release metódussal csökkentjük.
Ez lehetővé teszi, hogy a COM komponens saját maga tartsa nyilván, hogy hányan használják, vagyis, hogy hány használatban lévő interfészpointer van még, ami a komponens valamely interfészére mutat. Ha már egy sincs, a komponens kitöltődhet a memóriából.
A COM komponensek nyelv-független megvalósítására jellemző, hogy akár script-nyelvekből is elérhetők. A JScript nyelvben – mivel minden változó egy–egy objektum - így elegendő a CreateObject metódust meghívni a komponens példányának létrehozásához, paraméterként átadva a komponens azonosítóját (ProgId).
COM objektumok elérése .NET-ben
Amikor COM komponenseket és .NET objektumokat használunk egy alkalmazáson belül, a legfontosabb különbség a két objektum-típus között a memóriahasználatban van. A .NET objektumok a menedzselt memóriában találhatóak, melyet a Common Language Runtime (CLR) tart felügyelete alatt. Szükség esetén el is távolítja az objektumokat a memóriából.
A COM objektumok számára az úgynevezett nem-menedzselt memóriában foglalódik le terület, és várhatóan nem kerülnek át másik memóriaterületre életük során.
A Visual Studio és a .NET Framework karöltve biztosítanak eszközöket ahhoz, hogy a kétfajta memóriaterület között a szükséges kapcsolat megvalósuljon, vagyis a kétfajta objektumtípus képviselői kommunikálhassanak egymással.
Manapság az alkalmazások többsége használ COM komponenseket. Bár a .NET által megvalósított assembly-k elve némileg túlhalad a COM elméleteken, vannak esetek, amikor elkerülhetetlen használatuk. Ilyen eset például a Microsoft Office csomaggal telepített komponensek halmaza, melyek segítségével a legváltozatosabb funkciók is könnyedén elvégezhetőek. Amíg a COM komponensek alapvetően a Windows-os környezethez kötődnek, addig az assembly-kkel megvalósított szoftverfejlesztés a CLR kapcsán az önálló .NET Platformhoz, kihasználva annak számtalan előnyét.
A .NET Platformon történő fejlesztés megadja ugyanakkor annak lehetőségét, hogy már meglévő COM komponensek funkcionalitását beépítsük alkalmazásainkba. Ezt az úgynevezett COM interoperabilitás nevű technológia teszi lehetővé. A COM objektumok eléréséhez „Interop assembly”-ket használhatunk, melyek segítenek kapcsolatot építeni a menedzselt kód és a COM között.
A COM objektumok karakterisztikáját a típuskönyvtárak írják le, míg a .NET rendszerben az assembly manifeszt alkalmas arra, hogy egy menedzselt egység jellemzőit tartalmazza. A .NET típus importáló és exportáló eszközeinek segítségével (tlbimp.exe és tlbexp.exe) könnyedén generálhatunk típuskönyvtárakat assembly-kből, és importálhatjuk be a típuskönyvtárakban tárolt információt assembly-kbe. Ebből talán már látható, milyen könnyen hozhatóak „közös nevezőre” a két rendszer alkotói.
Az Interop assembly-k képeznek hidat a COM objektumok és a .NET assembly-k között, menedzselt tagokat rendelve a nem-menedzselt kód tagjaihoz (változók, struktúrák, stb.). Az összes, COM-al kapcsolatos művelet elvégzése az ő feladatuk. Ilyen művelet a MARSHALING. A műveletre azért van szükség, mert a COM komponensek metódusaiban a paraméterek, visszatérési értékek típusai nem minden esetben fedhetők le menedzselt kódbeli típusokkal, így a kezelő mintegy becsomagolja a COM típust, majd így adja vissza azt a hívó kódnak.
Interop Marshaling és COM marshaling
A legtöbb típusnak van egy reprezentációja mind a menedzselt, mind a nem-menedzselt memóriaterületen. Más típusok nem is rendelkeznek reprezentációval a menedzselt memóriaterületen, vagy kétértelmű típussal rendelkeznek. A kezelő (marshaler) segít ezen típusok összerendezésében.
A kétértelmű típusok esetén vagy hiányzik a típusinformáció (például egy tömb mérete), vagy több reprezentáció van jelen a nem-menedzselt memóriaterületen, és rendelődik egy önálló típushoz. Az ilyen típusokhoz a kezelőnek explicit utasításokat kell adjunk arra vonatkozóan, hogy milyen módon kezelje az adott típust, és az összerendelést.
A procedúra minden esetben lezajlik, amikor a hívó és a hívott objektum nem tud ugyanazon az adatpéldányon dolgozni. A módszer egyedisége, hogy a kétfajta objektum akkor is ugyanazon az adatpéldányon dolgozik, ha mindkettő rendelkezik egy-egy másolattal az ominózus adatból.
Az objektumok úgynevezett APARTMENT-ben – mint logikai egységben – működnek, mely lehet közös, de működhetnek két külön egységben is. Attól függően, hogy a menedzselt objektum, és a COM komponens azonos vagy különböző apartment-ben található, beszélhetünk Interop, vagy COM marshaling eljárásról.
Abban az esetben, ha az apartment közös, akkor az Interop marshaling automatikusan elvégez minden típus-összerendelést.
Amennyiben a két objektum külön apartment-ben található, akkor úgynevezett apartment-közi (cross-apartment) hívásra van szükség, vagyis mindkét típus-összerendezési eljárás megvalósul.
Amikor .NET-ben egy COM komponenst használni szeretnénk, akkor nem teszünk mást, mint a típuskönyvtárban definiált információkat egy assembly-be importáljuk a .NET tlbimp.exe eszközével, vagy egyszerűen referenciaként megadjuk a projektünkben, helyi másolatot (kész assembly-t) kapva eredményül. A COM típusok definíciói az assembly-be kerülnek, és a hívó alkalmazásban csak példányosítjuk a COM komponenst reprezentáló osztályt, vagy implementáljuk interfészét. Ekkor a komponensre való hivatkozások számát a CLR tartja nyilván (hívja meg az első szakaszban említett AddRef és Release metódusait az adott komponensnek), a nem-hivatkozott komponensek pedig a szemétgyűjtés (garbage collection) eredményeképpen eltűnnek a memóriából, akár csak egy menedzselt objektum.
Burkoló osztályok
A COM objektumokkal történő munka során kétféle hívásról beszélhetünk. A COM kliensek hívhatnak .NET objektumokat, vagy a .NET kliensek hívnak meg COM szervereket. A két híváskor kétféle, de mindenképpen szükséges burkoló objektumok jönnek létre. Ennek az oka, hogy a COM komponensek, és a .NET objektumok alapvetően különböző természetűek. Ezek vázlatosan a következők:
- COM komponensek esetén a hívó kliensek menedzselik a hívott objektum élettartalmát, míg a .NET rendszerben ez a feladat a CLR-re hárul.
- A COM kliensek úgy ellenőrzik a komponens használhatóságát, hogy küldenek egy kérést a használni kívánt interfésznek, és ellenőrzik, hogy kaptak-e vissza interfészmutatót, vagy sem. .NET esetében a kliensek a „Reflection” technológiát használják az objektumok elérhetőségének vizsgálatára.
- A COM komponensek kliensei csak egy pointert kapnak a komponensre (interfészeikre), így csak bízni tudnak abban, hogy az objektum azonos helyen marad a memóriában. Ellenőrzésre nincs lehetőségük. .NET rendszerben a hívott objektumok tetszőleges helyen lehetnek a memóriában, a futtatórendszer tökéletesen felügyeli azok mozgását.
Ezeknek a különbségeknek az áthidalására jönnek létre a burkoló osztályok, így mindkét objektumfajta előtt úgy tetszik, hogy egy azonos környezetben található objektumot hív meg. Amikor a menedzselt kód hív egy nem-menedzselt COM komponenst, akkor az úgynevezett Runtime Callable Wrapper (RCW) jön létre a futtatórendszer jóvoltából. Ellentétes irányú híváskor egy COM Callable Wrapper (CCW) jön létre, elsimítva a különbségeket.
Létező COM komponens elérése
A mellékelt WinCOMClient projektben egy létező COM objektumot érünk el, melynek segítségével képesek vagyunk avi állományokat lejátszani. Ekkor a gyorsmenüben az Add reference menüponttal hozzáadhatjuk a COM komponens típuskönyvtárából létrehozott assembly-t a projektünkhöz, majd meghívhatjuk az assembly-ben definiált osztály metódusát.
A felhasználandó COM komponenst a megjelenő ablak COM füle alatt választhatjuk ki, neve ActiveMovie control type library 1.0, mely a Windows rendszermappájában található QUARTZ.DLL néven.
A komponens neve QuartzTypeLib, melyet használnunk kell a forráskódban.
A komponens osztályát példányosítjuk első lépésben.
QuartzTypeLib.FilgraphManager graphManager = new QuartzTypeLib.FilgraphManager();
Majd előállítunk egy IMediaControl interfészt, melynek a RenderFile és a Run metódusai segítségével játsszuk le a mellékelt clock.avi állományt.
QuartzTypeLib.IMediaControl control = (QuartzTypeLib.IMediaControl)graphManager;
control.RenderFile("clock.avi");
control.Run();
COM objektum készítése, és elérése
A mellékelt COMClient és COMServer projektek tartalmazzák a megvalósított COM szerver és kliens-alkalmazásokat. A COMServer projekt tulajdonságai között be kell állítani, hogy a létrejövő assembly COM komponensként is üzemeljen. Ezzel a művelettel a REGASM.EXE alkalmazás funkciói válthatóak ki.
A szerver tartalmaz egy interfészt, melynek regisztrálásához megadunk egy GUID azonosítót.
[Guid("A1EB5A26-25CF-48c7-81D9-BE95BAF9D677")]
public interface ICOMInterface
{
string COMWrite(string name);
}
Az interfész egyetlen, szöveget visszaadó metódusát megvalósítjuk a Server osztályban, mely szintén tartalmaz egy azonosítót.
[Guid("EA4A2EFD-CC95-4d7a-AF70-4189B3ED7BA2")]
public class Server : ICOMInterface
{
public string COMWrite(string name)
{
return "Üdvözlöm, kedves " + name;
}
}
Az IDE Tools menüjéből elérhető OLE/COM Object Viewer alkalmazása segítségével meg is nézhetjük, hogy fordítás után megjelenik az objektum, mint regisztrált COM komponens. A .NET Categories alkönyvtárban kell keresnünk a COMServer.Server bejegyzést.
A COMClient alkalmazásban el is érhetjük a COM komponenst, anélkül, hogy a referenciát megadtuk volna a típusra. Ehhez egy példányt kell létrehoznunk a COMServer.Server ProgId felhasználásával.
Type t = Type.GetTypeFromProgID("COMServer.Server");
Majd aktiválnunk kell azt.
object obj = Activator.CreateInstance(t);
Egy objektum-tömbben megadjuk a COMWrite meghívandó metódus paraméterét, mely most a textBox1 szövegmezőben megadott név lesz.
object[] arguments = {textBox1.Text};
Majd a típus InvokeMember metódusával meghívjuk a példány tagfüggvényét.
object s = t.InvokeMember("COMWrite",BindingFlags.InvokeMethod,null,obj,arguments);
Az első paraméter tartalmazza a hívandó metódus nevét, a második paraméterben adjuk meg, hogy egy metódusról van szó.
A visszaadott értéket pedig karakterlánccá konvertálva visszaadjuk.
textBox2.Text = s.ToString();