Jak jste si jistě všimli, snažím se spíše publikovat řešení v co nejstručnější podobě (většinou konzolové aplikace). Je to z důvodu, že chci ukázat princip, do normální aplikace si to každý jistě vloží. V tomto trendu budeme dnes ještě částečně pokračovat.
Knihovny
V dnešní době je výhodné využívání sdílených knihoven .so (opětovné využití kódu, jednoduchá aktualizace a oprava chyb). Kylix vytváření sdílených knihoven samozřejmě podporuje.
Pro vytvoření sdílené knihovny stačí vybrat v menu File – New položku Shared library a posléze IDE vytvoří kostru sdílené knihovny.
Vytvoření sdílené knihovny
Sdílená knihovna se od programu liší klíčovým slovem Library místo Program. Dále se musí uvést seznam exportovaných funkcí uvozených klíčovým slovem Exports.
Vlastní knihovna (uložená v simple.dpr):
library simple; function Math_Name:Char; stdcall; // první funkce begin Result:='+'; end; function Math_Oper(a,b:Integer):Integer; stdcall; // druhá funkce begin Result:=a+b; end; exports Math_Name, Math_Oper; end.
Tímto jsme vytvořili jednoduchou sdílenou knihovnu s názvem libsimple.so, která exportuje celé 2 (slovy dvě) funkce a to s volací konvencí stdcall. O tom, že exportujeme naše dvě funkce nás přesvědčí readelf -a libsimple.so.
Poznámka: pokud neuvedeme exports, tak linker usoudí, že funkce jsou nepoužívané a jednoduše je nepřilinkuje.
Volací konvence
Postupem času se ustálilo několik možností, jak předávat parametry do procedur (volací konvence). Jelikož je to velmi důležité (zvláště, když používáme cizí knihovnu), uvedu malou tabulku:
Direktiva | Pořadí parametrů | Uvolnění | Používá registry |
---|---|---|---|
register | zleva doprava | rutina | ANO |
pascal | zleva doprava | rutina | NE |
cdecl | zprava doleva | volající | NE |
stdcall | zprava doleva | rutina | NE |
safecall | zprava doleva | runtina | NE |
Nejčastěji explicitně uvedenou konvencí v Linuxu bude cdecl – používají ji kompilátory jazyka C, tudíž, jestliže chceme použít knihovnu napsanou v Cčku, je nutno uvést tuto konvenci.
Pokud neuvedeme nic (skoro vždy), je implicitní volací konvence register (někdy označovaná jako fast-call), která umožňuje předat až tři parametry v registrech procesoru (EAX, EDX, ECX – zbytek je vložen na zásobník). Pokud chcete vědět, jakým způsobem je který typ předáván, koukněte do helpu na heslo Parameter passing.
Použití sdílené knihovny
Jelikož v Linuxu existuje spousta zajímavých knihoven, byla by škoda je nevyužít. Postup předvedu na výše uvedené knihovně, ale u jiných by to bylo podobné.
Nejdříve stvoříme „importní unit“ (uSimpleImp.pas):
unit uSimpleImp; // Importní unit pro sdílenou knihovnu libsimple.so interface function Math_Name:Char; stdcall; function Math_Operation(a,b:Integer):Integer;stdcall; implementation const clibsimple='libsimple.so'; // jméno knihovny function Math_Name:Char; external clibsimple; function Math_Operation(a,b:Integer):Integer; external clibsimple name 'Math_Oper'; end.
Všimněte si, že u druhé funkce jsem změnil jméno, takže i když je v knihovně uvedena jako Math_Oper, bude v programu vystupovat jako Math_Operation. Myslím, že je to naprosto jasné. Mimochodem: v implementační části nemusíme psát celou deklaraci.
Hlavní program je naprosto přímočarý (TestSimple.dpr):
program TestSimple; uses SysUtils, uSimpleImp in 'uSimpleImp.pas'; begin Writeln(Format('%d %s %d = %d',[1, Math_Name, 2, Math_Operation(1,2)])); end.
Plug-in
Nevíte, proč zásuvné moduly (jak zní prkenný překlad) používat? Pokud dobře navrhnete rozhraní, můžete očekávat, že i dlouho po vytvoření hlavního programu ho bude možno rozšiřovat (a to v různých programovacích jazycích). Příklady takových programů: ve Windows např. WinCommander, WinAmp, v Linuxu XMMS, Gnumeric a další.
Jelikož plug-in má určené rozhraní, tak se přímo nabízí implementovat ho jako objekt. S výhodou využijeme procedurální typ (viz minule a v následujícím příkladě). Pro spravování objektů s plug-iny použijeme nějakého následníka TList, nejlépe TObjectList, kde přidáme pár metod jako AddPlugIn, DeletePlugIn a nějaké property, které bude indexem vracet příslušný objekt. Že jsem se ještě o této možnosti u property nezmínil? Tak to hnedle napravíme:
type TPlugInWrapper=Class; // třída zapouzdřující plug-in nás nezajímá TPlugInsList=Class(TObjectList) protected function Get(Index:Integer):TPlugInWrapper; public function AddPlugIn(aPlugIn:TPlugInWrapper):Integer; procedure DeletePlugIn(Index:Integer); //read-only property property PlugIn[Index:Integer]:TPlugInWrapper read Get; end; implementation function TPlugInsList.Get(Index:Integer):TPlugInWrapper; begin Result:=TPlugInWrapper(Items[Index]); end; function TPlugInsList.AddPlugIn(aPlugIn:TPlugInWrapper):Integer; begin Result:=Add(aPlugIn); end; procedure TPlugInsList.DeletePlugIn(Index:Integer); begin Delete(Index); end; end.
Pokud máme u TPlugInWrapper definovaný destructor, tak ho ObjectList před svým vlastním uvolněním postupně zavolá pro všechny své položky. Je evidentní, že je to ideální místo pro uvolnění (nahrané) knihovny.
Poznámka: jednou jsem potřeboval napsat program, který by fungoval na principu plug-inů, ale s tím, že jednotlivé plug-iny se mohou zavést do paměti vícekrát. Jenže knihovny sdílí všechna data pro všechny instance knihovny v paměti (což je někdy výhodné), takže žádné globální proměnné (jako handly na soubory apod.) jsem nemohl používat. Vyřešil jsem to předáváním ukazatele na záznam, který byl zaalokován hlavním programem jako blok paměti (globalní proměnné se u každého plug-inu lišily) a plug-in pouze řekl, kolik paměti záznam zabírá, a paměť si přetypoval. Dále jsem ještě pro každý plug-in vytvořil kritickou sekci, aby se vždy od každého plug-inu prováděl v jednom okamžiku pouze jeden.
Demo aplikace
V další části zkusím napsat jednoduchou GUI aplikaci, která bude využívat plug-iny. Cílem je najít všechny plug-iny na zadané cestě a voláním exportované funkce Math_Name zjistit, jakou že to operaci implementuje. Předesílám, že mi jde spíše o princip než o eňoňuňo aplikaci. Pro dnešek ještě uvedu postup vytváření aplikace.
Vytvořte nový projekt a zároveň ho uložte pod nějakým jménem. Hlavní formulář uložte pod jménem fMain.pas. U hlavního formuláře změňte jméno na frmMain a zvolte nějakou šikovnou popisku (Caption). Poslední věcí u formuláře je nastavení vlastnosti BorderStyle na fbsDialog (u okna nelze měnit rozměry).
Nyní na připravený formulář vložte komponentu GroupBox a změňte její vlastnost Caption na „Plug-ins“. Na její plochu vložte komponentu ListView (se jménem lvPlugIn) a nastavte u ní ViewStyle na vsReport (tím budeme mít zobrazeno více sloupců). Po stisku pravého tlačítka nad vloženou komponentou se rozbalí menu, a z něj vyberete položku Columns editor. V něm vložíte tři položky a změníte jejich Caption na „Library name“, „Implemented function“ a „Size“.
Pod ListView vložíte komponetu Edit (edtPath) a dvě tlačítka (btnPath a btnFind) a upravíte je do stavu jako na obrázku. Můžete také upravit vlastnost Hint, která obsahuje text plovoucí nápovědy. V tom případě byste měli ještě nastavit ShowHint na True. Pojmenovávám pouze ty komponenty, na které se budu v kódu odkazovat. Pro jistotu připomínám, že pod jménem komponenty mám na mysli její vlastnost Name.
Program za běhu.
Tímto je uživatelské rozhraní prakticky hotovo a můžeme přistoupit k druhé části, kterou je oživení. Pokud v tomto okamžiku program spustíte, vypadá sice hezky (no dobře – beru zpět), ale nic nedělá.
Oživení bude spočívat v obsluze dvou událostí:
- OnClick u btnPath – způsobí výběr cesty do sousedního editboxu
- OnClick u btnFind – způsobí naplnění ListView nalezenými knihovnami
Výběr cesty
Uvedu přímo kód:
procedure TfrmMain.btnPathClick(Sender: TObject); var sDir:String; begin if SelectDirectory('Select directory',edtPath.Text, sDir,True) then edtPath.Text:=sDir+'/'; end;
Nalezení našich plug-inů
Toto je klíčový kód celého programu, takže jdeme na to:
procedure TfrmMain.btnFindClick(Sender: TObject); type TIDProc=function:Char; stdcall; // prototyp identifikacni funkce var sSearchFile:String; sr:TSearchRec; handle:Pointer; IDProc:TIDProc; // typ funkce begin lvPlugIn.Items.Clear; // smaž možné předchozí položky sSearchFile:=IncludeTrailingPathDelimiter(edtPath.Text)+'*.so'; if FindFirst(sSearchFile, faAnyFile,sr)=0 then repeat //nalezen nějaký soubor, zkusíme, zda je to naše knihovna handle:=dlopen(PChar(ExpandFileName(sr.Name)), RTLD_LAZY); if not assigned(handle) then Continue; try @IDProc:=dlsym(handle,'Math_Name'); // najdi naši funkci if assigned(@IDProc) then // je to naše knihovna, přidej ji do ListView with lvPlugIn.Items.Add do // vytvoř novou položku begin Caption:=sr.Name; // a vlož tam jméno knihovny { nelze nijak zjistit, zda parametry odpovídají - (tiše předpokládám, že mi nikdo nepodhodil .so se stejnou funkcí :)} SubItems.Add(IDProc); // to, co implementuje, zjistíme voláním SubItems.Add(IntToStr(sr.Size)) // a její velikost end; finally dlclose(handle); // zavři knihovnu end; until FindNext(sr)<>0; // a znovu end;
Jelikož budeme používat některá volání z libc, musíme nejdříve ještě doplnit do implementační části uses Libc;. Dáme to do implementační čísti, jelikož v deklarační části (interface) nic z tohoto unitu nepotřebujeme.
Pokud chceme mít úplně přenositelný kód, musíme pro funkce z Libc použít podmíněný překlad, nebo použít zapouzdření od www.delphi-jedi.org.
A ještě deklarujeme prototyp volání funkce v knihovně (v sekci Type).
A teď vlastní program. Smažeme případné položky v ListView a získáme zadanou cestu, ke které připojíme (pokud tam není) koncové lomítko a zadáme, že chceme všechny soubory, které končí na „*.so“. Pokud nějaký nalezneme, zkusíme zavést jako knihovnu. Pokud se nám povedlo zavedení, zkusíme v něm najít naši funkci a její adresu přiřadíme do procedurální proměnné IDProc. Nenulová adresa značí existenci symbolu.
Zde bych rád uvedl, že podle mě neexistuje jednoduchý způsob, jak zkontrolovat, že jde o naši funkci s naším počtem a typem parametrů. Prostě musíme zvolit dostatečně unikátní jméno této poznávací funkce (což v našem případě není).
Pokud se tedy jedná o naši knihovnu, přidáme do ListView jeden záznam (Add vrací nově vytvořený záznam – viz Help). Poznámka: SubItems jsou typu TStrings – viz. minulý díl.
Volání dlclose je v sekci finally, jelikož sekce finally se provádí vždy (při chybě i bez ní), ale dobře si všimněte, kde je try. Tento způsob je často používán, chybu sice nijak neočekáváme, ale když k ní nahodou dojde, jsou zabrané prostředky uvolněny.
Balíčky – packages
Kylix dále umožňuje vytvořit zvláštní druh knihoven, které se nazývají balíčky. Balíček obsahuje přeložený kód, který může být svázán s jiným balíčkem. Důležité je, že existuje několik balíčků, ve kterých je umístěna celá knihovna CLX a ve kterých se kód neopakuje.
Příklad: pokud vytvoříme normální program a k němu jednu knihovnu, každý z nich bude obsahovat určitý kód, který se bude v obou opakovat.
Pokud ale obojí vytvoříme tak, aby používaly balíčky, bude společný kód umístěn v balíčku.
Navíc pokud si vytvoříme vlastní balíček, můžeme u něj definovat závislosti na jiných balíčcích.
Zde musím uvést, že standardní balíčky jsou docela velké. Nastavení kompilace pro balíčky je v menu Project – Options – Packages – Run Time Packages. Výše uvedená GUI aplikace má přeložená s balíčky asi 33 Kb.
Vlákna
Kylix používá pro implementaci vláken knihovnu pthread. Můžete využívat všechny její možnosti, ale lepší je použít její zapouzdření, jak je nabízí Kylix ve třídě (TThread). Poznámka: samozřejmě lze použít také fork. Jednoduchý příklad uvedu někdy příště.
Zmíněná třída je abstraktní (nelze vytvořit její instanci). Pro využití je tedy nutno od zmíněné třídy zdědit. Pokud se vám nechce psát, lze v menu File – New zvolit vygenerování základní kostry objektu.
unit uTestThread; interface type TTestThread = class (TThread) protected procedure Execute; override; //předefinujeme klíčovou metodu end; implementation procedure TTestThread.Execute; var xIndex:Integer; begin //při ukončení této metody skončí vlákno xIndex:=0; repeat Inc(xIndex); sleep(100); //ať to chvilku potrvá until (xIndex>1000) or terminated; end; //konec vlákna end.
Je evidentní, že vlákno nic rozumného nedělá. Důležitá je proměnná terminated, která obsahuje požadavek na předčasné ukončení běhu vlákna. Samozřejmě, že se dá vlákno zabít i systémovým voláním, ale toto je čistější řešení.
Pokud chcete zajímavější příklad na vlákna, podívejte se do adresáře demos/threads.
Třída TThread publikuje několik zajímavých metod:
- konstuktor Create – má jeden parametr, který určuje, zda bude vytvořené vlákno pozastaveno
- Suspend – pozastaví spuštěné vlákno
- Resume – obnoví běh zastaveného vlákna
- Terminate – nastaví Terminated na True
- WaitFor – čeká na dokončení běhu
- Synchronize – zabezpečí serializaci provádění – používá se k přístupu k objektům CLX, které nejsou bezpečné vůči vláknům
Synchronizace provádění
S vlákny těsně souvisí jejich synchronizace. Kromě klasických přístupů, jak je nabízí Libc, zapouzdřuje Kylix některé synchronizační objekty, aby bylo dosaženo portability: TEvent, TMultiReadExclusiveWriteSynchronizer nebo TCriticalSection.
Komponenty – Reporty
Jak jsem již uvedl dříve, komponenty QuickReport nebyly portovány pro CLX, tudíž je nutné pro výstupní sestavy použít jiné komponenty.
Docela slušně vypadají komponenty FastReport, které jsou bohužel shareware. Na stejné stránce lze také stáhnout FreeReport, které jsou sice se zdrojáky, ale jen pod Windows. Na sourceforge se objevil projekt freereport, který je pod GPL (zatím jen pod Windows).
URL: www.fast-report.com
Existují také jiné komponenty, např. Synea.
Zatím nejlepší seznam komponent pro Kylix je k nalezení zde: www.torry.net/comp_klx_interface.htm
V porovnání s počtem komponent pro Delphi je to zatím slabší, i když některé komponenty pro Delphi se dají jednoduše upravit. Nezapomínejme, že Kylix je stár pouze něco přes rok. Jinak jsem tam narazil na komponenty, které by měly nahradit komponentu na vykreslení různých typů grafů z Delphi.