Jak jsem slíbil, tak činím. Pro zatím se ale nebudeme pouštět do žádných větších akcí ;) a tak se podíváme jak pracovat se soubory. Jako i jindy se lze k jednomu výsledku dopracovat několika způsoby, některé si tedy představíme.
22.9.2013 00:00 | Ondřej Tůma | přečteno 10602×
Ve vale, se k základní práci se soubory používá třída FileStream
. Vytváří se statickou metodou open
s parametry cesty k souboru a módem otevření, nebo fopen
s parametry file descriptor a mód. Tedy celkem standarní přístup. A jak by asi C / C++ programátoři očekávali, třída obsahuje standardní metody jako printf, putc, getc, eof, flush, seek, read, write
a další.
V minulém článku jsem zmínil základní virtuální soubory, běžně dostupné na všech posixových systémech stdin, stdout a stderr
. Tyto soubory jsou dostupné jako proměnné balíku glib
a proto je není nutné nijak inicializovat. A protože je GLib namespace implicitně dostupný, není třeba jej explicitně uvádět.
public static int main (string[] args){
char x = (char) stdin.getc();
stderr.printf("%s: Chybový text\n", args[0]); // Netřeba uvádět GLib namespace
GLib.stdout.printf("Zadali jste znak %c\n", x); // nic ale nezkazíme ;)
return 0;
}
Správnost si můžete ověřit například přesměrování jednoho z výstupů do /dev/null
, nebo do souboru:
~$ valac stdinouterr.vala
~$ echo E | ./stdinouterr
~$ echo K | ./stdinouterr 2>/dev/null
Třída FileStream
obsahuje běžné metody, které znáte z jiných jazyků, a které Vám umožní číst a zapisovat znaky, řádky nebo bloky dat. Pro čtení řádků se vysloveně hodí metoda read_line
, a pro zápis printf
s koncem řádku na konci.
public static int main (string[] args) {
// Otevře soubor filestream.vala pro čtení
FileStream stream = FileStream.open ("filestream.vala", "r");
assert (stream != null); // neúspěch ukončí aplikaci
string ? line; // obsah proměnné line může být roven null
// line je přirazena hodnota z read_line a ta je následně testována
// na null
while ((line = stream.read_line()) != null) {
stdout.puts(line); // do stdout vloží přečtený řádek
stdout.putc('\n'); // do stdout vloží znak nového řádku
}
return 0;
}
V uvedeném příkladu používám některé již probrané možnosti, ale raději je znovu vysvětlím. Nejprve ale od začátku. Otevřeme soubor filestream.vala
pro čtení, název i s módem otvírání je předat v parametrech statické metodě FileStream.open
.
Příkazem assert
otestujeme správné vytvoření třídy, ale vždyť stream
nemůže být null
?! Ve skutečnosti může. Deklarace bez otazníku způsobí, že na některých místech volá Vala assert
za nás, ne však všude. V našem příkladu tento assert test být nemusí, protože volání metody read_line
interně tento test stejně provede. Díky tomu by ani nemusel být string line
deklarován z otazníkem. Programátorovi i překladači by ale mělo být zřejmé, co v proměnné může být. Budeme chovat slušně a ono se nám to dříve, nebo později rozhodně vyplatí.
Konstrukce while
by neměla být ničím zajímavá, prostě načte díky metodě read_line
řádek, a ten pak testuje na správné přiřazení. Pokud vše proběhne jak má, na stdout
se vloží přečtený řádek a po něm i znak nového řádku, protože read_line
vrací znak bez něj.
Než se posuneme dál, zastavím se ale u metody read_line
. Možná byste místo složitého přiřazování a následného porovnávání přečteného řádku, chtěli použít metodu eof. Je třeba si ale uvědomit, že metoda read_line
ve skutečnosti čte znak po znaku (což můžete dělat také), a pokud narazí na znak '\n'
nebo na EOF
, ukončí čtení. A pokud do té doby nic nepřečetla (ani konec řádku), vrátí null
, nikoli prázdný string. Díky tomu, že většina editorů na konci posledního řádku, vkládá znak odřádkování a pak až znak EOF
, může se snadno stát, že EOF
přečtený nebude, a read_line
vrátí null
, takže kontrola metodou eof
nebude fungovat.
A co uzavření souboru ? FileStream
je třída, a ta má svůj konstruktor i destruktor, ten sice není vidět, ale volá se v době, kdy Vala zjistí, že už objekt nepotřebujete. Na místě tohoto destruktoru uvidíte ve vygenerovaném C souboru právě uzavření otevřeného souboru.
Za domácí úkol si zkuste tento primitivní příklad rozšířit o délku přečteného řádku, nebo o jeho číslo.
Dřív nebo později ovšem budete chtít číst, nebo zapisovat binární data. Logickým krokem, by pro Vás mohlo být použití metod read a write
. Jejich použití je ale malinko zrádné a je třeba u nich přemýšlet v čistém C.
Metody očekávají pole čísel o velikosti 8mi bitů (uint8
). Pokud máte data uložena v poli čísel, vlastně nemusíte druhý parametr zadávat, implicitní hodnota je 1. Pokud ale přetypováváte pole nějakých struktur, počet položek v poli se nemění, ale velikost struktury ano. Musíte pak uvést druhý parametr, ve kterém říkáte jak velká ta datová struktura je.
Problém může také nastat, pokud poslední čtení nepřepíše celý buffer. Prakticky se to dá obejít tak, že zpracovávat budete tu část bufferu, do které byla data skutečně načtena. Oba problémy si ukážeme v následujících příkladech:
public static int main (string[] args) {
// Otevře soubor filestream.vala pro čtení v binárním režimu
FileStream stream = FileStream.open ("filestream2.vala", "rb");
uint8 buf[20]; // pole 20ti byte
size_t size;
while ((size = stream.read(buf)) > 0) { // přečte jeden blok 20ti čísel
stdout.write(buf[0:size]); // do stdout vloží výsek z bloku
}
return 0;
}
Otevření souboru, resp. vytvoření objektu třídy FileStream
už známe, jen je doplněn mód o binární příznak. Tento příznak vlastně na posixových systémech nic nedělá, ale je dobré ho uvádět pro lepší orientaci. Následuje deklarace pole 20ti čísel o velikosti jeden byte. Toto pole by bylo v čistém C naprosto neinicializované, Vala ovšem vygeneruje kód, ve kterém inicializuje první hodnotu a tím donutí překladač nastavit všechny položky pole na hodnotu 0.
Konstrukce while
je velmi podobná předchozímu příkladu. Zajímavé je však volání metody read
. Metoda, jak už sem napsal, přečte pole čísel o velikosti definovaného pole, v našem případě tedy 20.
Následuje zápis pole do stdout
. Výsek z pole (buf[0:size]
) bude známý zejména pythonistům. Tento zápis vytvoří pole o nové velikosti, která je definovaná právě rozsahem v hranatých závorkách. Pokud by metoda write
dostala celé pole, zapsala by i část nepřepsaných dat přečtených v předchozím cyklu, což není žádaný stav. Nakonec ještě zmíním, že v případě metody printf
by toto samozřejmě nefungovalo, protože metoda printf
očekává string, tedy pole znaků, které je ukončeno hodnotou 0 (což nemusí být pravda) a nově vytvořené pole buf[0:size]
, by skoro určitě ukazovalo na původní data v paměti.
public struct Data { // jednoduchá datová struktura
char l;
char h;
public Data(char x) { // i struktura může mít konstruktor
l = x.tolower();
h = x.toupper();
}
}
public static int main (string[] args) {
// pole datových struktor
Data [] mem = {Data('a'), Data('h'), Data('o'), Data('j')};
stdout.write((uint8 []) mem, sizeof(Data)); // zapis pole struktur
// sizeof vrací velikost struktury Data v paměti
stdout.putc('\n');
return 0;
}
V první části programu definujeme veřejnou strukturu Data
. O strukturách jsem psal v úvodní části tohoto seriálu. Jde vlastně o velmi jednoduchou podobu objektu. Struktura má dvě vlastnosti l a h. V konstruktoru této struktury tyto vlastnosti nastavujeme.
a A h H
o O j J
mem
Magie začíná. V druhé části, tedy ve funkci main
, vytváříme pole těchto struktur. Pole má 4 prvky a každý prvek zabírá v paměti 2 znaky (byty). Přetypováním tohoto pole na pole bytů získáme zase pole o čtyřech prvcích. První byte ukazuje na první znak první struktury, druhý na druhy znak první struktury, třetí na první znak druhé struktury a čtvrtý na druhý znak druhé struktury. Ale protože potřebujeme zapsat všechny prvky v poli mem
, musíme metodě write
předat jako druhý parametr velikost jedné datové struktury. Metoda write
pak bude správně číst data z paměti od adresy proměnné mem
po adresu mem + mem.length * sizeof(Data)
.
sizeof
, věřte, že vrací velikost typu v bytech.uint8
.read a write
je bohužel velmi zavádějící chyba. Metody se opravdu chovají tak, jak jsem popsal, což si lze snadno ověřit studiem vygenerovaného C souboru.Než takový soubor otevřeme, je vhodné ho například otestovat, zda existuje. Někdy navíc potřebujeme se souborem dělat další „systémové“ věci. Kopírovat ho, vytvářet, měnit jeho parametry, získat jeho cestu atd. K tomu slouží třída File
z balíčku gio
. V minulém díle jsem naznačil cosi o dalších vala knihovnách – balíčcích. S těmi se budeme postupně setkávat, dnes použijeme první z nich. Něž k tomu však dojde, malinko odbočím.
Dalo by se říci, že balíček je synonymum pro knihovnu. Je třeba ale mít na paměti, že tomu tak vždy nemusí být. Autor dotyčného vapi (vala api) balíčku, může za jeden takový balíček schovat podporu několika souvisejících knihoven, nebo obráceně, jednu knihovnu rozdělit do několika balíčků. Standardní vala balíček odpovídá balíčku definovaného přes pkg-config
. Pkg-config je nástroj, který vrátí potřebné informace pro kompilování a linkování s danou knihovnou. Tento nástroj využívá celá rodina GNOME knihoven, od té nejspodnější – GLib, až po tu nejvyšší - GNOME. A možná nejen díky tomu, že podporuje i závislosti, nebo kontrolu verzí, existuje podpora pkg-configu napříč celým open-source světem. Jedna knihovna, má často několik samostatných balíčků, a tak do budoucna budu vždy uvádět pouze balíček, a případně i celou knihovnu, ze které balíček je.
Nyní už si můžeme ukázat malý kód na testování přítomnosti souboru.
public static int main (string[] args) {
// vytvoření třídy jedním z konstruktorů:
File file = File.new_for_path ("ctime.txt");
if (file.query_exists ()) { // kontrola zda soubor existuje
stdout.printf ("Soubor '%s' existuje\n", file.get_path());
} else { // nebo nikoli
stdout.printf ("Soubor 'ctime.txt' neexistuje\n");
}
return 0;
}
Uvedený příklad kompilujeme, je však nutné uvést balíček gio
, jež obsahuje třídu File
:
~$ valac file_exist.vala --pkg=gio-2.0
Co se vlastně v programu děje? Nejprve vytvoříme objekt file
, podobně jako v případě FileStream
, jednou ze statických metod. Třída File
má několik statických metod pro vytváření svých instancí, my vytváříme objekt z názvu souboru včetně cesty, konkrétně z relativní cesty. No a proč neuvádím namespace ? Protože balíček gio
patří do rodiny GLib knihoven, a tak používá GLib
namespace, který jak už jsme si řekli, je standardně integrován do všech vala programů.
Zbytek kódu testuje, zda soubor existuje, a pokud existuje, je vypsána celá jeho cesta. To znamená, že relativní cesta je doplněna o aktuální adresář, ve kterém se program pouští, a tedy ve kterém se soubor ctime.txt
nachází.
Třída File
obsahuje mnoho zajímavých metod, některé dokonce rovnou otvírají soubory pro čtení nebo zápis. Jsou to například metody append_to, crate, read
nebo jejich asynchroní modifikace. Tyto metody vrací objekty, které reprezentují opravdové soubory. Nejde však o FileStream
ale o InputStream, OutputStream
nebo IOStream
, který je zastřešuje, nikoli však objektově (oba objekty jsou vlastnostmi).
Tyto třídy jsou též součástí gio
balíčku, dědí z třídy GLib.Object
a tedy dědí i její vlastnosti jako je ref_count
nebo signal notify
. Všechny zmíněné třídy z gio
balíčku již pracují s výjimkami.
public static int main (string[] args) {
// Vytvoří třídu file, nikoli soubor test.log
File file = File.new_for_path ("test.log");
try { // v bloku může nastat výjimka
// až zde se vytvoří soubor test.log, pokud ovšem neexistuje
FileOutputStream os = file.append_to (FileCreateFlags.NONE);
// objekt string, má vlastnost data, která je čirou náhodou uint8[]
os.write ("Další nový řádek\n".data);
// odchycení výjimky GLib.Error
} catch (Error e) { // odchycení výjimky
stdout.printf ("Error: %s\n", e.message);
return 1;
}
return 0;
}
Jak je v ukázce uvedeno, vytvoření objektu file
rozhodně nevytváří žádný soubor. To je mimo jiné také důvod proč tato třída nemá žádnou metodu close
. Zato jeho metoda append
, již soubor vytváří, parametrem mu říkáme zda má soubor vytvořit přístupný všem, nebo jen uživateli, pod nímž je program spuštěn. O to jak se to děje na různých systémech se starat nemusíme, na každém to znamená něco jiného. Většina knihoven, které budeme používat a na kterých je Vala, resp, GTK, potažmo GNOME postaveno, obsahuje jistou míru abstrakce, díky níž nemusíte větvit program podle toho, na jakém systému byl spuštěn.
Třída FileOutputStream
má mimo jiných metodu write
, které se stejně jako v případě FileStream
předává pole čísel – bytů. A čirou náhodou objekt string
, i když je to objekt spíše abstraktní, obsahuje vlastnost data
, která se nám hodí. A ani zde soubor nezavíráme, i když nám to třída FileOutputStream
dovoluje. I zde platí že ztráta reference na objekt vyvolá destruktor, a ten se o vše postará sám. Ve vygenerovaném C souboru již žádné volání close
nenajdete, to proto, že jde o potomky třídy GLib.Object
a u nich se i v čistém C volá „destruktor“ funkcí g_object_unref
. To mimo jiné znamená, že tuto funkcionalitu mají třídy z gio
balíčku i ve své čisté C podobě.
K výjimkám se ještě dostaneme, proto jejich popis zatím vynechám a kód řádně otestujeme. Zkompilujeme, dvakrát spustíme, změníme práva výstupního souboru, a ověříme, že opravdu došlo k chybě.
~$ valac append_to_log.vala –pkg=gio-2.0
~$ ./append_to_log
~$ ./append_to_log
~$ chmod u-w test.log
~$ ./append_to_log
Balíček gio
toho však obsahuje daleko víc, než jen pár tříd pro práci se soubory. Umí pracovat i s disky, sockety, sítí, nebo třeba s uživatelským nastavením aplikace. Pokud budete potřebovat nějaký balíček, pomocí nějž byste otvírali soubory ze sítě, gio
je ten, který hledáte. Vedle gio
balíčku existuje ještě gio-unix
, ten obsahuje třídy, jenž využijete jen na unixových systémech (unix socket nebo disková přípojná místa).
Od příštího dílu již začneme probírat grafické rozhraní GTK+. Je toho celkem dost, tak doufám že se máte na co těšit. Na úplný závěr ještě zmíním, že gio
je součástí GLib knihovny (je distribuován pohromadě), tedy zatím používáme to, co je pro Valu nejzákladnější vybavení.
knihovna GLib
Vala dokumentace ke GLib.FileStream
knihovna GIO
Vala dokumentace gio balíčku
pkg-config
Příručka programu pkg-configl