Pisanie dla CPC6128 w roku 2023

Zabrałem się w wolnym czasie za nowy projekt i powoli powstaje nowe oprogramowanie, niebędące grą, na Amstrada CPC. Pomyślałem, że dzielenie się przemyśleniami z tego procesu może być interesujące.

Język i kompilator

Pierwszym wyborem, jakiego należy dokonać, to wybór języka, w którym w ogóle dane oprogramowanie ma być pisane. Podobnie jak w przypadku współczesnych platform (Mac, Linux, Windows), warto zadać sobie kilka pytań:

  • które języki programowania pomogą mi osiągnąć cel szybciej, a które wolniej?
  • czy z którymś z tych języków wiąże się dostępność bibliotek ułatwiających najczęstsze zadania?
  • czy któryś z tych języków posiada przyjemne w użyciu graficzne środowisko programistyczne?
  • jak debugować swój program?

Pisząc oprogramowanie, które na celowniku ma nowoczesne platformy, w pierwszym punkcie za języki pomagające mi (przyjazna składnia, czytelne, mało kodu niezbędne do osiągnięcia dobrych wyników, kompilator zdolny wyłapać wiele błędów programisty) uznałbym Pythona, TypeScript, C#, może nawet Javę (przymykając okno na “mało kodu, duży efekt”), za niesprzyjające – asembler, Go, czysty JS, ruby (bo go nie znam, a wszystko, co mam w rubym żre gigabajty pamięci), PHP (tylko dlatego, że nikt nie będzie chciał czytać potem tego kodu), gdzieś pośrodku plasowałyby się C, Ada i Object Pascal (nie utrudniają pracy).

Dla platform 8-bitowych sytuacja wygląda trochę inaczej. O czymkolwiek, co wymaga środowiska uruchomieniowego lub wprowadza takie wewnętrznie, możemy zapomnieć. Dynamiczne, kompilowane w locie języki — nie ma na to ani pamięci, ani mocy obliczeniowej. Potrzebujemy struktur danych, które niosą jak najmniej dodatkowych informacji dodanych nie wprost, możliwie wczesnego przetworzenia do jak najbardziej wykonywalnej i zoptymalizowanej postaci, a jeżeli chcemy, żeby kompilator też działał na docelowej platformie, to język musi pozwalać na dość proste parsowanie i rozwiązywanie wszystkich zależności między symbolami. Daje to przede wszystkim dużo wariantów Pascala, C, asembler i Forth.

Postanowiłem zacząć od języka wyższego poziomu, żeby nie od razu musieć się przejmować najmniejszymi detalami, z możliwością korzystania ze wstawek w asemblerze Z80.

Tak, wiem, prawdziwi mężczyźni piszą w asemblerze. Prawdziwi mężczyźni nie kupują miodu, tylko żują pszczoły.

SDCC

Pierwszy kandydat: SDCC (Small Device C Compiler). Mamy tu kompilator języka C umiejący kompilować na “małe komputery”, z czego najciekawsze dla nas będą MOS 6502 (Commodore) i Z80 (Amstrad, Spectrum, MSX).

Środowiska programistyczne? Nie te największe, ale SDCC jest obsługiwany przez CodeBlocks, Eclipse, ale kompilatora tego używa także dedykowane środowisko i SDK w jednym — CPCTelera.

Projekt jest o dziwo wciąż utrzymywany. Mówię “o dziwo”, bo gdy coś ma stronę domową na SourceForge, to automatycznie myślę, że ostatnia aktualizacja była 15 lat temu.

Zalety:

  • C, uniwersalny język programowania znany od 1972
  • działa na Windows, Linux, Mac OS
  • nie wymusza konkretnej platformy docelowej (maszyny ani systemu operacyjnego),
  • optymalizuje kod, stara się możliwie najlepiej dobierać rejestry procesora Z80 do zastosowania.

CPCTelera

Mamy tu całe SDK (zestaw wielu narzędzi i bibliotek) do Amstrada, stworzone głównie z myślą o tworzeniu gier (nie oszukujmy się, jestem w mniejszości, wierząc, że warto tworzyć na 8-bitowce cokolwiek innego).

opis funkcji cpct_mode_rom_status w dokumentacji online frameworka CPCTelera
przykład funkcji CPCTelera prosto z jego dokumentacji

Zasadniczo nadal piszemy kod z myślą o SDCC jako kompilatorze, ale wiele problemów zostało już za nas rozwiązanych – np. rysowanie i animowanie tzw. sprite’ów (tych małych grafik przedstawiających bohatera lub przeciwników), odtwarzanie muzyki w formacie (dołączonego) Arkos Trackera, tworzenie gotowych obrazów dyskietek, konwersje wielu formatów grafiki i dźwięku, albo na niższym poziomie – szybsze wyświetlanie tekstu niż systemowe.

Cechy:

  • Działa na Windows, Linux, Mac OS
  • Tu już mówimy jednak tylko o Amstradzie CPC (Plus, ewentualnie).
  • Część zawartych bibliotek ma komentarze, które pomogą… hiszpańskojęzycznym programistom.

W CPCTelera stworzyłem na przykład drugą wersję animacji ekranu tytułowego dla swoich filmów.

z88dk

W następnej kolejności przyglądałem się i dokonałem kilku prób z użyciem zestawu narzędzi z88dk, który właściwie też pod spodem używa kompilatora sdcc.

Jako jeden z niewielu pozwala za pomocą jednego przełącznika kompilować dla systemu CP/M, co budzi nadzieje na bardziej przenoszalny kod (co oznaczałoby, że możemy go wydać przy znacznie mniejszych modyfikacjach na inne komputery obsługujące CP/M, jak Commodore 128, Spectrum +3, Altair, Apple II, BBC Micro, Epson… można tak wyczerpać niemal cały alfabet. Kuszące!

z88dk zawiera tak naprawdę dwa kompilatory, sccz80 i zsdcc, a to, który z nich zostanie użyty, zależy od argumentów przekazanych do głównego narzędzia zcc… więc sprawa dość szybko robi się skomplikowana.

Cechy:

  • Też działa wszędzie
  • Też C i Z80 asm
  • Obsługa CP/M i kilka różnych SDK na różne platformy

Mój build zawierał tu mały Makefile definiujący, jak skomplikować kod (które z mnogich przełączników od platform zastosować), jak stworzyć obraz dyskietki (używając iDSK, osobnego narzędzia), potem tę dyskietkę wystarczyło w emulatorze odmontować, zamontować na nowo, zresetować, uruchomić… i testujemy!

ccz80

To narzędzie jest pewnym odchyleniem od ścisłego trzymania się standardów. Chociaż nazwa ccz80 może sugerować, że to jakiś „C compiler for Z80”, w rzeczywistości mówimy tu o osobnym języku programowania. Jest on oparty o język C, ale też nieco zmodyfikowany i uproszczony (chociażby nie ma potrzeby deklarowania funkcji main). Wraz z kompilatorem dostajemy małe IDE:

ekran z przykładowym programem dla Amstrada CPC w ccz80

Składnia upraszcza też określenie, czy dana funkcja wymienia dane poprzez argumenty na stosie (domyślnie), rejestry procesora lub też czy ma zostać wpisana bezpośrednio w wywołujący kod zamiast wywołana (inlining). Takie typowe rzeczy, o które trzeba się martwić, kiedy pamięci, cyklów procesora i jego rejestrów mamy malutko. Całość przychodzi z bibliotekami zapewniającymi najczęściej potrzebne funkcjonalności dla systemów/platform CPC, CP/M, MSX i Spectrum.

Wstawki w asemblerze banalnie prosto napisać, nie trzeba nawet mówić, że to asembler, wystarczy po przecinku wstawić jego instrukcje jako ciągi tekstowe w ciele funkcji. 🤷🏻‍♂️

Cechy:

  • Napisany w .NET
  • Prosty, ale jednak niestandardowy język.
  • Dokumentacja właściwie nie istnieje.
  • Obsługuje różne platformy docelowe, w tym system CP/M.

Przerwa na kawę i przemyślenia

Wszystkie pakiety, o których mówiłem dotąd, trzymają się dość blisko jednego języka, C. Warto tu trzykroć podkreślić, że mówimy o C, a nie C++. Moje wspomnienia z programowania na CPC dekady temu były raczej obrazkami pisania nieczytelnego kodu w BASICu (czytelniejszy bywał w zeszycie niż na ekranie) i czytania listingów z „Bajtka” z przykładami także w Turbo Pascalu, marząc, że i mi byłoby dane go używać.

Na nowocześniejszych platformach najwięcej doświadczenia miałem raczej z C++ w bardzo małych ilościach, raczej modyfikując istniejące fragmenty, niż pisząc w nim projekt od zera. Może zaczynałem też od Object Pascala (dawniej zwanego językiem Delphi). Szybko jednak przytuliłem języki i środowiska wprowadzające więcej wbudowanych mechanizmów zarządzania pamięcią lub dość frywolne podejście do typów danych. Zawodowo najwięcej czasu spędziłem z TypeScriptem (JavaScriptem), Pythonem, Javą, GoLangiem, eksperymentowałem też kiedyś z C#.

Potrafię więc pisać proste programy, np. na potrzebę jakiegoś zadania algorytmicznego, w C++, ale czy potrafię wytrzymać tworzenie w nim całego oprogramowania? Czy będę zawsze pamiętać o zwolnieniu pamięci, o zadeklarowaniu każdej funkcji albo przed jej użyciem, albo dwa razy, aktualizując potem osobno jej deklarację i jej definicję (z ciałem, czyli kodem do wykonania)? Czy przypomnę sobie, że wypadałoby wydzielać te deklaracje do plików nagłówków z rozszerzeniem „.h” i czy zrozumiem w końcu, czym się różni #include "file.h" od #include <file.h>? Być może…?

Ale C to jednak nie C++, nie mamy tutaj nie tylko klas1, ale i pewnych pojęć, które po iluś latach w branży bierze się za pewnik — na przykład typu boolowskiego i stałych true i false. Przypomnienie sobie, że C nie rozumie w kodzie słowa true, było pewnym szokiem i momentem kontroli rzeczywistości. Podobnie odczułem fakt, że w tym języku też najpierw trzeba wypisać wszystkie zmienne, których dana jednostka kodu zamierza używać, a potem można zawrzeć już wyłącznie logikę, bez możliwości zadeklarowania zmiennej tuż przed jej użyciem. Do tego dołóżmy jeszcze fakt, że funkcja jako argument może przyjmować wskaźnik do danej, ale może też przyjmować stały wskaźnik (const char *buffer) i zaczynasz się zastanawiać, co to właściwie w praktyce dla kompilatora może oznaczać… albo czy typ int to na Twojej maszynie 8, 16, a może 32 bity?

Może jeżeli nie jestem pewien, co robię na poziomie samej maszyny, czy wiem, co potrafi biblioteka, którą wybrałem, ani jak daną rzecz na 8-bitowcu zrobić najwydajniej („ooo, to wypisanie linijki tekstu może trwać TAK DŁUGO?”), to nie powinienem dokładać sobie jeszcze egzystencjalnych wątpliwości na poziomie każdego detalu składni języka?

Może możliwość targetowania każdego komputera poprzez celowanie w CP/M nie jest dla mnie najważniejsza, jeśli moje rozwiązanie nie było jeszcze sprawdzone na żadnym?

1. Programowanie obiektowe nadal może mieć się dobrze, bo moim zdaniem polega przede wszystkim na właściwym operowaniu strukturami danych.

CPCBasic Compiler

Tu na scenę wkracza, cały na zółto i na niebiesko, CPC Basic Compiler, tego samego autora, co ccz80, który, prawdę mówiąc, też leży bardziej „po tej stronie” przemyśleń z poprzedniej sekcji.

strona domowa projektu CPC Basic

Mimo że mowa o języku CPC Basic, jest to cross-compiler, czyli — tak jak poprzedni kandydaci — narzędzie, którego używamy na innej platformie (PC) niż docelowa (CPC).

Oprogramowanie to jest napisane w języku C#, a więc działa pod kontrolą platformy .NET, co samo w sobie jest ciekawym wyborem (dalekim od niskopoziomowego celu kompilacji). W tym miejscu zauważam, że sam ccz80 też jest napisany dla .NET.

Sam interfejs też jest dość nietypowy. Trochę udaje ekran Amstrada, trochę edytor kodu z kolorowaniem składni — górna część to stylizowana na ekran startowy komputera plansza tylko-do-odczytu prezentująca automatycznie sformatowane polecenia i fragmenty kodu, które wpiszemy w pole wejścia w dolnej części okna.

Kiedy wpiszemy RUN, zamiast uruchomienia, program zostanie skompilowany i zapisany na dysku w katalogu Outputs (w formacie .bin, czyli wykonywalny kod maszynowy gotowy do uruchomienia dla Amstrada). Możemy tu łatwo zmienić format pliku wyjściowego na źródła w asemblerze, gotowy obraz dyskietki, snapshot maszyny, jak i bezpośrednie uruchomienie w emulatorze.

Taka koncepcja jest wyjątkowo ciekawa. Wydawałoby się, że pozwala używać języka, który mamy świetnie udokumentowany w samym podręczniku komputera, użyć istniejącego już kodu napisanego w BASICu, ale sprawić by działał znacznie szybciej, a sam proces jest bardzo prosty w obsłudze (przynajmniej etap kompilacji). Zamiast szukać wywołań API specyficznych dla danego frameworka, można przebierać w rozbudowanych standardowych (i niejednokrotnie lepiej udokumentowanych) poleceniach sterujących grafiką, dźwiękiem, tekstem i urządzeniami wejścia-wyjścia.

Należy jednak mieć się na baczności! Strona kompilatora wymienia wiele różnić w zachowaniu skompilowanego programu w porównaniu z tym samym kodem napisanym bezpośrednio w „prawdziwym” BASICu. Wiele z nich jest dość istotne, na przykład brak zatrzymania programu w przypadku niekontrolowanego błędu, zmienne liczbowe są wartościami bez znaku (wyłącznie dodatnimi), obsługa liczb rzeczywistych jest zastąpiona własną tablicą takowych, w związku z czym nie można używać większości funkcji matematycznych (ale dostajemy w zamian symulowane polecenia RSX, czyli zamiast x = SIN y można użyć… o rety… |CREAL,0,x:|CREAL,1,y:|SIN,1,2… po czym odczytać moglibyśmy tę wartość jedynie jako liczbę całkowitą lub reprezentację w postaci tekstu), deklaracje tablic, symboli i zmiennych są globalne… lista jest naprawdę długa.

Czyli jednak to nie do końca jest CPC Basic, wbrew nazwie.

Turbo Rascal Syntax error, “;” expected but “BEGIN”

Nie, nie wkleił mi się komunikat o błędzie, to jest nazwa języka i narzędzia programistycznego! Bywa też skrótowo nazywane „Turbo Rascal Syntax Error” (TRSE) lub po prostu “Turbo Rascal”, co po polsku oznaczałoby… turbo szelmę. 🙂

Mamy tu bardziej kompletne IDE (środowisko programistyczne), czyli kompilator i edytor zintegrowany w jedno, wraz z innymi przydatnymi narzędziami.

TRSE powstawało jako narzędzie do tworzenia gier przede wszystkim na Commodore 64, ale z czasem dodano obsługę procesora Z80, a co za tym idzie, platform takich jak Amstrad, ZX Spectrum, GameBoy.

wideo promocyjne środowiska Turbo Rascal pokazujące stworzone w nim dema na różnych platformach

Jak sama nazwa sugeruje, język, w którym piszemy, będzie tu bardzo zbliżony do języka Turbo Pascal, jednak też zmodyfikowany. Podobnie jak w przypadku ccz80 mamy więc podobieństwo do standardowego i uniwersalnego języka, ale jednak z dostosowaniem go do konkretnych zastosowań. W obu przypadkach oznacza to zapewne, że będziemy przywiązani jednak do ich „matczynych” edytorów, nie mogąc skorzystać z jakiegoś bardziej zaawansowanego nowoczesnego IDE (nie jest to duża wada, można jednak mieć przyzwyczajenia, że narzędzia, których używamy, umieją na przykład zmienić nazwę zmiennej czy funkcji w każdym miejscu, w którym została użyta, ale nie ruszając innych nazwanych przypadkiem identycznie rzeczy w kodzie).

To środowisko wydaje się mieć pewną przewagę — zintegrowany edytor służy pomocą i dokumentacją, która w pozostałych przypadkach często bywa trudniej dostępna:

Z trudniej strony na załączonym zrzucie ekranu widać, że nie wszystko wygląda tak, jak w języku Pascal – na przykład definicja typu rekordu odbywa się de facto z użyciem słowa kluczowego var, a z type wręcz nie zadziała… Czyli jednak nie do końca mamy standardowy język programowania i po raz kolejny należy się liczyć z pewnymi odstępstwami, w imię możliwości pisania w miarę lekkiego kodu na niezbyt rozbudowane systemy komputerowe.

Można natomiast mieć nadzieję, że jest to jedno z narzędzi, które pozwolą dość łatwo napisać wspólną logikę dla wielu platform, a specyficzną dla danego komputera rozdzielić, wciąż implementując ją w tym samym środowisku programistycznym.

Dodatkowym atutem wyróżniającym się na wczesnym etapie korzystania z tego środowiska jest także to, że jednym przyciskiem (F5) możemy skompilować i uruchomić nasz program w emulatorze – bez osobnego tworzenia pliku, umieszczania go na obrazie dyskietki, a następnie tejże dyskietki w emulowanej stacji dysków i bez wpisywania ręcznie polecania RUN "MYTHING", lecz wprost do wykonania kodu (tylko jeden emulator to obsługuje, Caprice32, więc jest wymagany jeśli chcemy w ten sposób testować swój kod).

Opinie?

Które narzędzie przekonało Was najbardziej? Ja dam szansę TRSE.

Bibliografia

SDCCSDCC – Small Device C Compiler (sourceforge.net)
z88dkz88dk/z88dk: The development kit for over a hundred z80 family machines – c compiler, assembler, linker, libraries. (github.com)
CPCTeleralronaldo/cpctelera: Astonishingly fast Amstrad CPC game engine for C developers (github.com)
ccz80Programming language ccz80
CPCBasic CompilerCPC Basic
Turbo RascalTurbo Rascal Syntax Error (TRSE) – LemonSpawn
gdzie szukać kompilatorów z tego artykułu


StackOverflow: Czemu kompilatory C średnio umieją w Z80? Why do C to Z80 compilers produce poor code? – Retrocomputing Stack Exchange

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments