O kompatybilności wprzód i jej dziwactwach

2011-06-11 15:36

Oto proste zadanie. Proszę sobie przypomnieć dowolną funkcję z dowolnej platformy/biblioteki/frameworka/itp. posiadającą przynajmniej jeden parametr, który nigdy nie został przez nas użyty w żadnym jej wywołaniu. Idealnie by było, gdyby ów argument występował na innej pozycji niż ostatnia – tak, żeby jego pominięcie wymagało podania zera, NULL-a, nulla, nila, Nila, None-a, pustego napisu/tablicy/listy lub dowolnej innej, “neutralnej” wartości.
Jeżeli udało nam się uporać z tą łamigłówką, to mam kolejną w postaci prostego pytania: dlaczego takie funkcje w ogóle istnieją? Zadając to pytanie na głos, często doczekamy się odpowiedzi, które niewiele mają wspólnego z prawdą. Bo istnieją rzadkie sytuacje, gdy ów felerny parametr jednak się przydaje. Bo nie ma innego dobrego miejsca, gdzie można by go umieścić. Bo język programowania jest kiepski i tak już musi być. Bo w sumie co to za kłopot z tym jednym nullem więcej. Bo.. bo… – i tak dalej.

W rzeczywistości prawidłową odpowiedzią jest zazwyczaj brzydkie słowo na ‘k’, czyli kompatybilność :) Pół biedy, jeśli chodzi tutaj o kompatybilność wsteczną – ona jest codziennością co bardziej złożonych aplikacji i systemów oraz wszelkich bibliotek, które mają swoją własną historię. Nie da się jej uniknąć i zwykle nawet nie warto próbować, chociaż oczywiście okresowe jej porzucanie jest w gruncie rzeczy wskazane.
Jest jednak jeszcze kompatybilność wprzód. Dziwna to idea, mająca na celu przewidywanie przyszłych wymagań wobec systemu i ułatwiająca lub wręcz umożliwiająca sprostanie im. Należy więc ona do dziedzin z pogranicza prorokowania i prognoz pogody, zatem z miejsca wydaje się cokolwiek podejrzana. Co więcej, potrafi się ona wkraść do projektu tak podstępnie i niezauważenie, że nie musi być nawet świadomie stosowana, by zostawić w nim swoje ślady. Ślady zupełnie niepożądane, o czym jednak przekonujemy się zwykle dopiero dużo później.

Mniej więcej coś takiego zdarzyło mi się ostatnio podczas tworzenia wersji androidowej swojej gry sprzed paru lat, czyli Taphoo. Sprawa dotyczy formatu niewielkich (<1 kB) plików, w którym obie wersje przechowują poszczególne etapy gry. Nie należy on w żadnym razie do przesadnie skomplikowanych, gdyż jest prostym formatem binarnym, składającym się z trzech części:

  • stałego nagłówka o rozmiarze dwóch bajtów, równego zawsze 54 4C (w ASCII są to znaki TL, będące skrótem od Taphoo Level)
  • wymiarów etapu: szerokości i wysokości, zapisanych na dwóch bajtach każda
  • właściwej planszy, której każda komórka zajmuje dokładnie jeden bajt


Format etapów w Taphoo

Oryginalnie format ten służył jedynie windowsowej wersji gry, ale nie widziałem żadnych powodów, dla których nie mógłbym użyć go także w powstającym porcie na platformę Android. Z pewnością znaczącym czynnikiem było to, że razem z pierwotną wersją gry napisałem też do niej – nie chwaląc się – bardzo dobry edytor :) Rozsądne było więc, aby przepisać kod wczytujący poziomy na Javę i włączyć go do gry w wersji androidowej.
Intuicja każe przypuszczać, że wtedy właśnie pojawiły się kłopoty. Faktycznie tak było. W związku mam jeszcze jedną, ostatnią dzisiaj zagadkę, która powinna być stosunkowo łatwa do rozwiązania na podstawie informacji podanych wcześniej. Co mianowicie jest przykładem niepotrzebnej kompatybilności wprzód w opisanym wyżej formacie i jakie problemy może to stwarzać podczas portowania?…

Bez zbędnych wstępów spieszę z wyjaśnieniem, że problem tkwi w sposobie, w jaki są zapisywane wymiaru etapu, a więc jego długość i szerokość. Ponieważ każda z tych wielkości zajmuje 16-bitowe słowo, pojawia się tutaj kwestia kolejności bajtów w zapisie binarnym, zwana potocznie endianess. Jak wiadomo istnieją tutaj dwa warianty (little endian i big endian) i między poszczególnymi platformami sprzętowym panuje raczej spora dowolność jeśli chodzi o to, który z nich jest domyślnym. Chcąc więc zapewnić przenośność danych binarnych pomiędzy różnymi platformami, należy ustalić preferowaną kolejność dla wartości wielobajtowych. Stąd na przykład wynika istnienie standardu sieciowego w tej kwestii oraz odpowiednich funkcji konwertujących w bibliotekach socketów. Nie muszę chyba dodawać, że mój “standard” radośnie pomijał tę kwestię całkowitym milczeniem ;)
Wniosek wydaje się więc taki, iż należało to wcześniej doprecyzować, aby pozbyć się opisanej wyżej niejasności. Gdzie jednak jest obiecana kompatybilność wprzód, która okazała się zbędna?… Otóż tkwi ona w samym fakcie, że wymiary etapu zapisywane są na 16-bitach! Przypomnijmy sobie, że maksymalna wartość 16-bitowej liczby bez znaku to 216-1, czyli 65535. To ogromny zapas, pozwalający teoretycznie na przechowywanie etapów o absurdalnych rozmiarach i wielkościach plików rzędu gigabajta
Jak to się ma do rzeczywistości, gdzie żaden taki plik przekracza kilkuset bajtów? Oczywiście nijak. W praktyce zupełnie wystarczające byłoby zapisywanie każdego z wymiarów na jednym bajcie, bo rzeczywiste etapy rzadko są większe niż 10×10 pól. Przy takim rozwiązaniu nie rzecz jasna żadnych problemów z endianess. Znika też wspomniana “kompatybilność wprzód”, która wkradła się do formatu za sprawą z pozoru nic nieznaczącego wyboru. W końcu kto by się dzisiaj zastanawiał, czy liczbę zapisywać na jednym, dwóch czy nawet czterech bajtach?…

Na tym przykładzie widać jednak, że zastanowić się warto, bo zjawisku wprowadzania kompatybilności wprzód trudno jest zapobiec. W istocie należy się przed nim aktywnie bronić, zawczasu ustalając granice stosowalności naszego rozwiązania tak wąsko, jak tylko się da. W przeciwnym razie będziemy mieli kłopoty, po którym zwykle zostają resztki: quirki czyli dziwactwa, takie jak wspomniane na początku zbędne parametry w funkcjach. W formacie etapów Taphoo takim quirkiem są wymiary zapisane na dwóch bajtach, z których pod uwagę brany jest tylko jeden, bo drugi musi być zerem. Świat się oczywiście od tego nie zawalił, ale zawsze to jakaś niezręczność :)

Tags: , , , , ,
Author: Xion, posted under Programming »


5 comments for post “O kompatybilności wprzód i jej dziwactwach”.
  1. olo16:
    June 11th, 2011 o 17:54

    Dobrze prawisz, ale przydałby się jakiś bardziej “spektakularny” przykład – w końcu endianess da się załatwić szybko i łatwo, więc problemu praktycznie nie ma. Ale jak trafi się na coś gorszego to kaplica.

    Z tak poza tym: jak się zapisuje dane bo pliku, to trzeba przewidzieć format od razu ;P. Nie wiem jak to wyglądało w kodzie u ciebie, ale wersja najprostsza – ja jakbym zobaczył (char*)&liczba, to już by mi się zapaliło czerwone światełko…

  2. Xion:
    June 11th, 2011 o 19:44

    Raczej coś w stylu Stream.Write(liczba);. Tak, oryginalna windowsowa wersja Taphoo była pisana w Delphi :)

  3. B@ss:
    June 20th, 2011 o 22:57

    Cześć. Mam taką małą prośbę do Ciebie, czy mógłbyś umieścić kawałek kodu odpowiedzialny za zapis poziomów do Taphoo z edytora? Zależy mi na tym, chciałbym wiedzieć jak Ty to zaimplementowałeś.

    Będzie taka możliwość?

  4. Xion:
    June 25th, 2011 o 14:12

    No cóż, rzecz nie jest jakoś skomplikowana:
    [delphi]function TEditableBoard.Save(const AFileName: String) : Boolean;
    var
    Stream : TFileStream;
    i, j : Integer;
    begin
    try
    try
    Stream := TFileStream.Create(AFileName, fmCreate or fmShareExclusive);

    Stream.WriteBuffer (‘TL’, 2);

    Stream.WriteBuffer (Word(FWidth), SizeOf(Word));
    Stream.WriteBuffer (Word(FHeight), SizeOf(Word));

    for j := 0 to FHeight – 1 do
    for i := 0 to FWidth – 1 do
    Stream.WriteBuffer (Byte(FBoard[i, j]), SizeOf(Byte));
    except
    Result := False;
    Exit;
    end;
    finally
    FreeAndNil (Stream);
    end;

    Result := True;
    end;[/delphi]
    Kod ten odpowiada zresztą dokładnie temu, co pisałem nt. formatu etapów w notce.

  5. B@ss:
    June 25th, 2011 o 15:02

    Dzięki bardzo!
    Liczyłem że kod będzie w C/C++ ale w Delphi też może być :).

Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.