Sesja 17 — Pliki: program przestaje żyć tylko w kodzie źródłowym

✅ Sprawdź się z sesji 16

Cel

Wczytać “program” dla maszyny wirtualnej z pliku na dysku, zamiast wpisywać go na sztywno w kodzie C. To dokładnie to, co zrobi chip8_load_rom w ćwiczeniu 1 CHIP-8 — dziś ćwiczycie tę samą operację na prostszym formacie danych.

Teoria w pigułce

FILE * i podstawowe operacje

#include <stdio.h>

FILE *f = fopen("program.txt", "r");   // "r" = do odczytu, tekstowo
if (f == NULL) {
    fprintf(stderr, "Nie mozna otworzyc pliku\n");
    return 1;
}

// ... odczyt ...

fclose(f);   // ZAWSZE zamykaj plik, gdy skonczysz -- to jak "free" dla malloc

fopen zwraca NULL, jeśli plik nie istnieje albo nie ma do niego dostępu — zawsze to sprawdzajcie, dokładnie tak jak sprawdzaliście wynik malloc w sesji 12. To ten sam wzorzec defensywnego programowania, powtórzony na nowym zasobie systemowym.

Odczyt liczb z pliku tekstowego: fscanf

int kod, rejestr, wartosc;
while (fscanf(f, "%d %d %d", &kod, &rejestr, &wartosc) == 3) {
    // przetworz jedna instrukcje
}

fscanf działa jak scanf z sesji 2, tylko czyta z pliku (f) zamiast z klawiatury. Zwraca liczbę pomyślnie wczytanych wartości — sprawdzenie == 3 to sposób na wykrycie końca pliku albo złego formatu danych, zamiast zgadywać.

Odczyt danych binarnych: fread

Pliki .ch8 w CHIP-8 to dane binarne (surowe bajty), nie tekst — do nich używa się innej funkcji:

uint8_t bufor[4096];
size_t wczytano = fread(bufor, 1, 4096, f);
// bufor[0..wczytano-1] zawiera zawartosc pliku

fread(cel, rozmiar_elementu, liczba_elementow, plik) czyta surowe bajty — nie próbuje interpretować ich jako tekst czy liczby w formacie czytelnym dla człowieka, w odróżnieniu od fscanf. To dokładnie funkcja, której użyjecie w chip8_load_rom.

NoteFormat tekstowy dziś, format binarny w CHIP-8 — dlaczego różnica?

Dziś, dla wygody testowania i czytelności, Wasz “program” to zwykły plik tekstowy z liczbami oddzielonymi spacjami. Prawdziwe ROM-y CHIP-8 to binarne pliki .ch8 — dokładnie ten format, w jakim gry z lat 70. faktycznie krążyły. Zasada wczytywania (otwórz, sprawdź błąd, czytaj, zamknij) jest identyczna — różni się tylko funkcja odczytu (fscanf dla tekstu, fread dla bajtów) i to, czy trzeba osobno “parsować” wczytany tekst (do tego wracamy w sesji 18).

Zadania

Zadanie 1 — zapisz testowy program do pliku

Ręcznie (w edytorze tekstu) utwórz plik program.txt z kilkoma instrukcjami Waszej maszyny w formacie kod rejestr wartosc, po jednej na linię, np.:

1 0 10
2 0 5
3 0 0

(gdzie 1 = LOAD, 2 = ADD, 3 = PRINT — dopasuj do swoich wartości enum).

Kontrakt formatu: trzy liczby na instrukcję

Każda instrukcja ma pola kod rejestr wartosc. Nie zapisujemy surowej pamięci struktury. Przypisz jawne wartości w enum zgodnie z tabelą; zachowaj te same wartości w interpreterze i plikach danych.

Kod Operacja rejestr wartosc
1 OP_LOAD docelowy literał
2 OP_ADD docelowy literał
3 OP_PRINT odczytywany 0
4 OP_SUB docelowy literał
5 OP_MUL docelowy literał
6 OP_ADD_REG docelowy indeks rejestru źródłowego
7 OP_JUMP 0 docelowy indeks instrukcji
8 OP_CMP porównywany literał
9 OP_JUMP_IF_ZERO 0 docelowy indeks instrukcji
10 OP_PUSH_REG odczytywany 0
11 OP_POP_REG docelowy 0
12 OP_CALL 0 docelowy indeks instrukcji
13 OP_RET 0 0

Jeśli wcześniej dodano inne operacje, nadaj im osobne kody od 14 i opisz ich argumenty. Nie zmieniaj znaczenia istniejących kodów. Nieobsługiwany kod jest błędem.

Próba operacji między rejestrami:

1 0 9
1 1 14
6 0 1
3 0 0
3 1 0

Oczekiwane wyniki: 23 i 14. W trzecim wierszu 1 oznacza R1, nie literał odejmowany ani dodawany bezpośrednio. Dla tej operacji trzeba sprawdzić zakres obu indeksów. Dla skoków i CALL sprawdź adres po wczytaniu całego programu; CALL musi wskazywać istniejącą instrukcję. Zwykły skok do indeksu równego liczbie instrukcji oznacza zakończenie. Nieużywane argumenty muszą być zerami.

Zadanie 2 — wczytywanie z pliku zamiast tablicy w kodzie

Zmień main z sesji 16, żeby zamiast tablicy struct Instrukcja program[] = {...} wpisanej w kodzie, wczytywał instrukcje z pliku podanego jako argument wiersza poleceń: ./maszyna program.txt. Skorzystaj z argc/argv, tak jak w kodzie startowym ćwiczenia 1 CHIP-8 (int main(int argc, char **argv)), i z malloc/realloc z sesji 12, bo nie znacie z góry liczby instrukcji w pliku.

Zadanie 3 — obsługa błędnego pliku

Sprawdź, co się stanie, gdy podasz nieistniejącą nazwę pliku, i upewnij się, że program wypisuje czytelny komunikat błędu (nie crashuje bez wyjaśnienia). Sprawdź też plik z błędnym formatem (np. tekst zamiast liczb) — czy Wasze sprawdzenie fscanf(...) == 3 poprawnie to wykrywa?

Kryterium sukcesu

  • Maszyna wczytuje program z pliku podanego jako argument wiersza poleceń, nie z kodu źródłowego.
  • Program kończy się czytelnym komunikatem błędu (nie awarią) przy nieistniejącym pliku.
  • Rozumiesz różnicę między fscanf (tekst, sparsowany od razu na liczby) a fread (surowe bajty) — i wiesz, której użyjecie w ćwiczeniu 1 CHIP-8.