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