C dla każdego (cz. 19.)
Pliki
Dziś zajmiemy się obsługą plików. Jednak zanim to
nastąpi, musimy napisać kilka słów wstępu.
W systemie możemy wyróżnić: pliki, katalogi,
urządzenia i przypisania, czyli "assigny". Mamy
nadzieję, że każdy wie, czym są pliki i katalogi.
O urządzeniach, takich jak "DF0:", zapewne też
każdy słyszał.
Natomiast jest kilka innych
urządzeń, o których warto wspomnieć:
NIL: -- urządzenie puste. Wszelkie dane zapisane
do niego natychmiast giną. Na przykład możemy
skierować strumień wyjściowy programu do "czarnej
dziury", pisząc tak: "copy C: ram: >NIL:", i nie
zobaczymy żadnych komunikatów.
PRT: -- drukarka, dane tu skierowane mogą zawierać
sekwencje sterujące, które zostaną przetłumaczone
przez driver ustawiony w preferencjach. Listę
sekwencji można znaleźć w dokumentacji Workbencha.
CON: -- konsola, czyli okienko do pogaduszek z
użytkownikiem. Proponuję następujący eksperyment:
copy CON:10/10/100/100/Pisz...
CON:110/20/100/100/Czytaj...
Naszym oczom ukażą się dwa okienka: w jednym
możemy pisać, a z drugiego czytać. Zapewne
zauważyliście, że informacje w drugim oknie
pojawiają się dopiero po naciśnięciu przycisku
[Return], funkcjonuje również historia (strzałki w
górę i w dół). Jest to analogiczne do AmigaShella,
który używa pliku "CON:". Aby przerwać działanie
"papugi", wystarczy przycisnąć kombinację
przycisków [Ctrl][\], która powoduje, że
instrukcja "copy" poda stwierdzenie o zakończeniu
pliku źródłowego (skoro już przy tym jesteśmy, ta
sama kombinacja kończy również działanie Shella).
Po współrzędnych i nazwie okna można umieścić
dodatkowe flagi, rzecz jasna, oddzielone znakiem
"/". Najczęściej używane to AUTO, WAIT, CLOSE,
SCREEN. Pierwsza powoduje, że okno pojawia się
dopiero wówczas, gdy jest potrzebne, to znaczy
przy pierwszej próbie odczytu lub zapisu. Kolejna
flaga powoduje, że okno nie zniknie po zamknięciu
pliku. Trzeba to zrobić ręcznie. Flaga CLOSE
wymusza pojawienie się gadżetu zamykania,
"SCREENnazwa" zaś wymusza otwarcie konsoli na
ekranie publicznym o nazwie "nazwa", doklejonej do
flagi SCREEN.
RAW: -- konsola "surowa" jest podobna do "CON:",
jednak w jej wypadku dane docierają natychmiast,
bez oczekiwania na potwierdzenie za pomocą
[Return].
SER: -- port szeregowy. Domylnie respektowane są
ustawienia z preferencji, można je jednak
zmieniać:
copy C:copy SER:9600/8N1
9600 to prędkoć transmisji (w bodach) za 8N1
oznacza 8 bitów danych, brak bitu parzystości (No
parity), jeden bit stopu (patrz program
preferencji "Serial").
PIPE: -- "potok", jest urządzeniem właściwie
nieznanym, a wyjątkowo przyjemnym. Z jednej strony
potoku wpisujemy informacje, a z drugiej
odczytujemy je. Dzięki temu nie trzeba tworzyć
plików tymczasowych, a programy mogą działać
równolegle. "Zróbmy prawdziwy test":
run list DH0: ALL >PIPE:spis
more <PIPE:spis
Dzięki temu jeden program będzie listował dysk, a
drugi wypisywał dane. Mimo że "more" zatrzyma się
po wypisaniu całej strony, "list" nadal będzie
napełniał potok.
Napis po dwukropku jest opcjonalną nazwą potoku
(nie może zaczynać się od cyfry), następnie po
znaku "/" można umieścić rozmiar bufora na dane, a
po kolejnym znaku rozdzielającym liczbę buforów
(zero oznacza dowolną liczbę). Wykorzystanie
potoków jest niezwykle powszechne w systemach typu
Unix, gdzie można z Shella w postaci jednej linii
uruchomić całą kaskadę programów połączonych
potokami. Wystarczy nazwy programów oddzielać
pionowymi kreskami, a wówczas wyjście pierwszego
jest kierowane do wejścia drugiego itd. Warto w
tym miejscu wspomnieć, że można to osiągnąć
również i na Amidze, korzystając np. z programu
Pipe, który znajdziemy w Aminecie
(util/shell/Pipe-1.5.lha). W naszym wypadku efekt
analogiczny do poprzedniego przykładu możemy
uzyskać pisząc:
list DH0: ALL | more
Należy pamiętać, że urządzenie "PIPE:" musi być
przed użyciem zamontowane. Zwykle komputer zrobi
to sam, podczas wykonywania sekwencji startowej.
Assign, czyli przypisanie
Używanie przypisań daje większą elastyczność
pracy: pliki nie muszą być umieszczane w
określonych z góry miejscach. Wystarczy do
katalogów, w których się znajdują, utworzyć
przypisanie o określonej nazwie. Przypisania
takie, jak "C:", "LIBS:", "DEVS:" itp., są używane
przez system operacyjny. Przypisania ułatwiają
programowanie, dlatego też wiele programów wymaga
jednego lub więcej. Są one jednak przeznaczone
przede wszystkim dla ludzi. Zbyt dużo
"zaśmiecających", potrzebnych programom przypisań,
powoduje, że użytkownikowi trudno znaleźć tych
kilka jemu przydatnych. Aby tego uniknąć, twórcy
systemu 2.0 stworzyli pseudo-przypisanie
"PROGDIR:", które jest inne dla każdego procesu.
Jest ono ustawione na katalog, w którym znajduje
się kod uruchomionego programu (a więc np. "list
PROGDIR:" wylistuje zawartość katalogu "C:").
"PROGDIR:" jest eleganckim i preferowanym sposobem
dostępu do zewnętrznych danych programu.
Wróćmy jednak do przyjemniejszych tematów.
Przed przystąpieniem do odczytu lub zapisu plików
musimy poinformować system o naszych zamiarach.
Służy do tego funkcja Open() z biblioteki
"dos.library" (przypominamy, że kompilator sam
otwiera tę bibliotekę):
BPTR Open( STRPTR name, long accessMode );
Pierwszym argumentem funkcji jest nazwa pliku
(wraz ze ścieżką dostępu). Argument "accessMode"
określa sposób dostępu do pliku, określony za
pomocą stałych z pliku "dos/dos.h":
MODE_OLDFILE -- otwarcie istniejącego pliku
(możliwy jest zarówno odczyt, jak i zapis).
MODE_NEWFILE -- otwarcie nowego pliku. Jeli plik
nie istnieje, to jest tworzony, w przeciwnym
wypadku stary plik jest wymazywany, pojawia się
nowy plik o zerowej długości.
MODE_READWRITE -- otwarcie istniejącego pliku,
jeli istnieje, w przeciwnym wypadku plik jest
tworzony.
Funkcja Open() zwraca zero w wypadku
niepowodzenia. Jeli wszystko poszło dobrze, to
zwrócona wartość jest wskaźnikiem BPTR na strukturę
"FileHandle", która jest ważna bardziej dla
systemu niż dla nas. My używamy jej jako
identyfikatora pliku podczas operacji
wejścia-wyjścia.
BPTR jest pozostałością z czasów, kiedy DOS był
napisany w języku BCPL. Jest to adres pamięci
podzielony przez cztery. DOS wewnętrznie operuje w
takiej notacji, z tego powodu niektóre struktury
muszą mieć adresy podzielne przez cztery.
Do odczytu danych z pliku służy funkcja:
LONG Read( BPTR file, APTR buffer, long length );
Jej argumentami są: wartość zwrócona przez Open(),
wskaźnik na obszar pamięci, w którym dane mają
zostać umieszczone, oraz liczba bajtów do
odczytania. Funkcja zwraca liczbę odczytanych
bajtów. Jeli jest ona mniejsza od "length" (może
być nawet zerowa), to oznacza, że plik się
skończył, jeli natomiast wynosi -1, to znaczy, że
nastąpił błąd odczytu.
Do zapisu służy funkcja:
LONG Write( BPTR file, APTR buffer, long length );
Argumenty analogiczne, jak powyżej. Zwraca liczbę
zapisanych bajtów albo -1, gdy nastąpił błąd (w
tym wypadku o błędzie można mówić również wtedy,
gdy nie udało się zapisać wymaganej liczby
bajtów).
Aby dopełnić opisu podstawowych funkcji, należy
jeszcze opisać funkcję "zamykającą plik", której
jedynym argumentem jest wartość zwrócona przez
Open():
LONG Close( BPTR file );
Dostęp do plików jest sekwencyjny. Aby ułatwić
poruszanie się po nich, system oferuje funkcję
pozwalającą "przesunąć" się w inne miejsce pliku.
Miejsce to nazywa się umownie kursorem.
LONG Seek( BPTR file, long position, long offset
);
Argumentami są kolejno: identyfikator pliku, nowa
pozycja kursora (w bajtach) oraz tryb wyznaczania
nowej pozycji w pliku:
OFFSET_BEGINNING -- "position" jest określona
względem początku pliku;
OFFSET_CURRENT -- "position" jest określona
względem bieżącego położenia;
OFFSET_END -- "position" jest określona względem
końca pliku.
Funkcja zwraca poprzednią pozycję w pliku
(względem początku) lub -1 w wypadku wystąpienia
błędu.
Na razie omijalimy problem błędów, które mogą
nastąpić podczas obsługi wejścia-wyjścia.
Informacje na temat natury zaistniałych błędów
można uzyskać wywołując funkcję:
LONG IoErr( void );
Zwraca ona liczbę będącą kodem błędu, spis kodów
znajduje się w pliku "dos/dos.h".
Ponieważ numery błędów niewiele użytkownikom
mówią, w wersji 2.0 systemu operacyjnego powstały
funkcje tłumaczące je na postać zrozumiałą dla
przeciętnych zjadaczy chleba:
LONG Fault( long code, STRPTR header, STRPTR
buffer, long len );
BOOL PrintFault( long code, STRPTR header );
Pierwsza umieszcza opis błędu we wskazanym
obszarze pamięci, druga wpisuje go do strumienia
wyjściowego.
Ich argumentami są: "code" -- numer zaistniałego
błędu, "header" -- nagłówek tekstu, który ma
zostać wypisany (np. nazwa pliku, przy którym
wystąpił błąd) oraz, w wypadku pierwszej funkcji,
wskaźnik na obszar pamięci, w którym napis ma być
umieszczony (buffer) oraz wielkość tego obszaru
(len).
Wartością zwróconą przez pierwszą funkcję jest
liczba znaków wpisanych do bufora (może być 0, gdy
błędu nie było). Długość komunikatu o błędzie nie
może przekraczać wartości "FAULT_MAX" zdefiniowanej
w pliku "dos/dos.h".
Kolejnymi problemami nurtującymi programistów są
znalezienie rozmiaru pliku i wylistowanie
katalogu.
Pierwszym krokiem na drodze do ich rozwiązania
jest zablokowanie dostępu do pliku lub katalogu, w
celu ustrzeżenia się np. przed usunięciem badanego
przez nas obiektu w trakcie badania. Służy do tego
funkcja:
BPTR Lock( STRPTR name, long type );
Pierwszym jej argumentem jest nazwa obiektu,
drugim typ założonej blokady:
SHARED_LOCK lub ACCESS_READ -- dopuszczamy
czytanie dla innych;
EXCLUSIVE_LOCK lub ACCESS_WRITE -- uzyskanie
pełnego dostępu;
Jeli operacja się powiedzie, otrzymamy liczbę
niezerową (wskaźnik BPTR do struktury "FileLock",
która nie powinna nas interesować), w przeciwnym
za wypadku wartoć zero.
Kolejnym krokiem w poznaniu naszego pacjenta jest
zbadanie go funkcją:
LONG Examine( BPTR lock, struct FileInfoBlock
*fileInfoBlock );
Jej argumentami są wartość zwrócona przez Lock() i
wskaźnik na strukturę "FileInfoBlock" (patrz
"dos/dos.h"), która zostanie wypełniona danymi.
Adres struktury "FileInfoBlock" musi być
koniecznie podzielny przez cztery, czyli np. być
przydzielony przez AllocMem(). Funkcja Examine()
zwraca wartoć różną od zera, gdy wszystko poszło
OK, i zero w przeciwnym wypadku.
Interesującymi polami struktury "FileInfoBlock"
są:
fib_DirEntryType -- w polu tym jest odnotowane,
czy "obiekt" jest katalogiem (wartoć większa do
zera) czy plikiem (wartoć mniejsza od zera).
fib_FileName -- tablica zawierająca nazwę pliku,
napis jest zakończony znakiem o kodzie zero.
fib_Protection -- bity protekcji HSPARWED, dla
każdego z nich istnieje maska bitowa.
fib_Size -- rozmiar pliku w bajtach.
fib_NumBlocks -- rozmiar pliku w blokach.
fib_Date -- data ostatniej modyfikacji --
struktura "DateStamp", zawierająca trzy składniki:
-- ds_Days -- liczba dni po 1 stycznia 1978 roku,
-- ds_Minute -- liczba minut po północy,
-- ds_Tick -- liczba "tyknięć" wewnętrznego zegara od
początku minuty, liczba przypadająca na sekundę
jest zdefiniowana w "dos/dos.h jako
TICKS_PER_SECOND. Od wersji 2.0 istnieje funkcja
DateToStr() zamieniająca "DateStamp" na czytelną
postać.
fib_Comment -- komentarz zakończony znakiem o
kodzie 0.
Aby wylistować katalog, należy założyć na niego
blokadę, zbadać go za pomocą Examine(), a
następnie w pętli wywoływać funkcję:
LONG ExNext( BPTR lock, struct FileInfoBlock
*fileInfoBlock );
Parametry i zwracana wartoć analogiczne jak przy
Examine(). Rezultatem działania ExNext() jest
wypełnienie struktury danymi, opisującymi obiekt
znajdujący się w badanym katalogu. Wywołując
funkcję wielokrotnie, będziemy otrzymywać dane
kolejnych obiektów. Nie wolno modyfikować
zawartości "FileInfoBlock", aby nie zaburzyć
procesu listowania. Funkcja ta zwraca wartoć
niezerową, gdy wszystko poszło dobrze, i zero w
przeciwnym wypadku. Gdy zostaną już zbadane
wszystkie pliki, ExNext() poinformuje o błędzie.
Należy więc sprawdzić za pomocą IoErr(), czy jest
to błąd "ERROR_NO_MORE_ENTRIES".
Aby umożliwić innym pracę po listowaniu, należy
zdjąć blokadę za pomocą funkcji:
void UnLock( BPTR lock );
Jej jedynym argumentem jest wartoć zwrócona przez
Lock().
Wszystkie używane do tej pory funkcje
wejścia-wyjścia są funkcjami nie buforowanymi, w
związku z tym ich praca bywa w niektórych
wypadkach mało efektywna. Twórcy systemu wzięli to
pod uwagę i w systemie 2.0 wprowadzili funkcje
posługujące się buforem. Nie będziemy ich tu
opisywać dokładnie, gdyż ich składnia jest niemal
identyczna z funkcjami ANSI C, różnią się jedynie
pisownią (np. użyty w przykładzie Printf()).
Należy jednak być ostrożnym i nie mieszać tych
funkcji z funkcjami nie buforowanymi bez
opróżniania bufora za pomocą funkcji Flush().
Ponieważ funkcje dostarczane przez kompilator nie
korzystają z udogodnień systemu, należy traktować
je jako osobną grupę, mającą własny bufor.
Listing nr 14
Listing nr 15