Debugowanie preprocesora

2008-06-18 22:48

Preprocesor w C++ to może i przestarzała, ale i całkiem fajna zabawka. Przynajmniej czasami - mimo dość ubogiej logiki - pozwala ona załatać choć niektóre braki języka. A niekiedy pozwala też na pewne sztuczki, jakie ciężko byłoby osiągnąć inaczej. W końcu, można go też używać do tego, w czym jest najlepszy: do automatycznego wklejania powtarzających się fragmentów kodu w wielu miejscach.
Ponieważ jednak preprocesor działa na kodzie jak na tekście, to rezultaty mogą być czasami zadziwiające - zwłaszcza dla kompilatora. Może on nam wówczas pokazać błąd w linijce, która jest całkowicie poprawna... Ale tylko z dokładnością do zawartego w niej makra, które po rozwinięciu generuje błąd składniowy lub semantyczny. Weźmy chociażby chyba najprostszy przypadek tego typu:

#define COUNT 10;
int buf[COUNT]; // błąd!

W nim łatwo o wykrycie pomyłki, lecz w rzeczywistych sytuacjach może to być zadanie nie lada. Zwłaszcza wtedy, gdy użyte makro jest skomplikowane, a my przecież nie możemy przełączyć preprocesor w tryb pracy krokowej i śledzić, jak jest ono rozwijane.

Możemy jednak przynajmniej podejrzeć ostateczne rezultaty. Wprawdzie domyślne każdy plik źródłowy po przetworzeniu przez preprocesor trafia od razu do kompilatora, jednak zachowanie to można łatwo zmienić. Możliwe jest na przykład zrzucenie wstępnie przetworzonego źródła do pliku, który to możemy potem przejrzeć i zobaczyć, w co ostatecznie nasze makra się zmieniły.
W Visual C++ takie zachowanie ustawia się we właściwościach projektu, na zakładce Configuration Properties > C/C++ > Preprocessor. Tam możemy wybrać, czy chcemy, by rezultaty działania preprocesora na plikach .cpp były zapisywane do osobnych plików (z rozszerzeniem .i). Wachlarz opcji umożliwia też określenie, czy mają być do nich dołączane numery linii lub komentarze.

Opcje preprocesora w Visual C++

Włączenie tych opcji bywa przydatne, jeśli dostajemy na linijkach kodu z użyciem makr dostajemy komunikaty o błędach kompilatora, które są zupełnie "z innej bajki". Ponadto w ten sposób można też zobaczyć, jakie fragmenty kodu są kierowane do kompilacji za pomocą dyrektyw typu #ifdef, co też bywa przydatne.
Pamiętajmy jednak, że wygenerowane w ten sposób pliki .i są prawie zawsze bardzo, bardzo duże - rzędu kilku megabajtów - gdyż zawierają treść wszystkich dołączonych nagłówków. Dlatego, ze względu na szybkość budowania projektu, nie powinniśmy ich generować poza przypadkami debugowania preprocesora.

Tags: ,
Author: Xion, posted under Applications, Programming » Add comment

Lis w operze

2008-06-16 16:47

Logo FirefoksaParę dni temu odbyła się premiera nowej wersji przeglądarki Opera, oznaczonej numerkiem 9.5. Z kolei jutro z wielką pompą opublikowana zostanie trzecia edycja Firefoksa. Przy tej zresztą okazji Mozilla organizuje Dzień Pobierania, z zamiarem ustanowienia rekordu ilości ściągnięć oprogramowania w ciągu jednej doby. (Mam aczkolwiek przeczucie, że prawdopodobne jest ustanowienie innego rekordu: najdłużej niedziałającego serwera pobierania ;]).
Zbieżność tych dwóch wydarzeń zapewne nie jest przypadkowa. Tym bardziej dziwi mnie to, że przy tej okazji zaprzysięgli fani któregoś z tych dwóch programów zajmują się głównie wykazywaniem w każdym możliwym miejscu, iż to właśnie ich ulubiona przeglądarka jest lepsza od tej drugiej. Naturalnie, argumentów jest przy tym mnóstwo: a to gesty myszki, a to współdzielenie zakładek i Ulubionych między komputerami, nie wspominając o tysiącach dostępnych rozszerzeń i wtyczek, coraz lepszych wynikach testów typu Acid3, wbudowanych klientach innych usług niż WWW, kilogramach skórek i pewnie wielu jeszcze innych zaletach. Część z nich może i jest ważna i interesująca. Rzecz w tym, że - statystycznie rzecz biorąc - nie obchodzą one psa z kulawą nogą.

Logo OperySmutna prawda jest taka, że nadal większość użytkowników Internetu korzysta z "jedynie słusznej" przeglądarki pewnej znanej skądinąd firmy, a o dywagacjach na temat wyższości innych nad jeszcze innymi nigdy tak naprawdę nie słyszeli i raczej nie mają zamiaru usłyszeć. A to z wielu powodów niepokojące, chociażby ze względu na bezpieczeństwo czy powszechność stosowania standardów sieciowych. Wzajemne zwalczanie się zwolenników przeglądarek "niezależnych" (że je tak umownie nazwę...) na pewno nie zaradzi temu, iż połowa internautów wciąż używa do przeglądania Sieci programów przestarzałych i dziurawych jak ser szwajcarski.
A bliskość premier nowych edycji dwóch bezpiecznych, wygodnych i nowoczesnych przeglądarek to przecież całkiem dobra okazja, żeby coś na to poradzić. Zamiast więc bezowocnie spierać się, czy Safari jest lepsze od Opery, a ta od Firefoksa czy Camino, można by wspólnie zwiększyć wysiłki na rzecz uświadamiania użytkownikom niesłusznie najpopularniejszych przeglądarek, że istnieją dla nich o wiele lepsze alternatywy. Cóż bowiem z tego, że oto "wszyscy" przerzucą się czy na to Firefoksa, Operę czy inny tego typu program, jeśli ci 'wszyscy' to tak naprawdę ledwie 1/3 internautów?...

Tags: , ,
Author: Xion, posted under Applications, Internet, Thoughts » 10 comments

Stos wywołań funkcji

2008-06-13 13:42

Jedną z bardziej przydatnych informacji podczas znajdowania przyczyn błędów wykonania jest śledzenie stosu (stack trace; właściwie to nie ma dobrego polskiego tłumaczenia, jak zwykle zresztą :]). Dzięki niemu możemy mieć informacje o kolejnych wywołaniach funkcji, które doprowadziły błędu wykonania. Zwykle stack trace jest dostępny z poziomu złapanego obiektu wyjątku - np. poprzez właściwość System.Exception.StackTrace w .NET czy metody java.lang.Throwable.get/printStackTrace w Javie.

Standardowa klasa exception w C++ nie posiada podobnej funkcjonalności, bo w tym języku stos nie jest automatycznie śledzony. Przy pewnym wysiłku możemy aczkolwiek zapewnić sobie podobną funkcjonalność - chociażby tak:

typedef std::list<std::string> StackTrace;

class Trace
{
    private:
        static StackTrace ms_Trace;

    public:
        Trace(const std::string& item) { ms_Trace.push_back (item); }
        ~Trace()    { ms_Trace.pop_back(); }

    friend const StackTrace& GetStackTrace()    { return ms_Trace; }
};
StackTrace Trace::ms_Trace;    // inicjalizacja pola statycznego (w .cpp)

// makro (dla Visual C++)
#define GUARD Trace __trace(__FUNCTION__ " [" __FILE__ ", line " __LINE__ "]")

Przechowujemy po prostu globalną listę z danymi wszystkich wywoływanych funkcji. Elementy do niej dodawane są przy wejściu w każdą funkcję, która na początku ma makro GUARD:

int Foo() { GUARD; /* ... */ }

zaś zdejmowane wtedy, gdy pomocniczy obiekt typu Trace wyjdzie poza swój lokalny zasięg - czyli po opuszczeniu funkcji. To sprawia, że na liście mamy zawsze informacje o wszystkich wywołaniach funkcji prowadzących do aktualnego miejsca (jakie one są, zależy od kompilatora; użyte wyżej makro __FUNCTION__ nie jest na przykład standardową częścią C++). Tworząc obiekt wyjątku, możemy więc zapisać kopię tej listy, by można ją było odczytać w bloku catch i wyświetlić.
Rozwiązanie nie jest oczywiście bardzo ładne, bo wymaga dodatkowej linijki w każdej funkcji. Zdaje się jednak, że nie ma żadnego sposobu, by tę niedogodność wyeliminować. Ja przynajmniej takiego nie znam :)

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

Kontenery haszujące w Visual C++

2008-06-11 21:10

W standardowej bibliotece pojemników (STL) języka C++ jest póki co poważny brak. Nie ma mianowicie kontenerów opierających się na mechanizmie haszowania (hashing), znanego też pod mniej jednoznaczną nazwą mieszania. Mówiąc zupełnie najprościej, takie pojemniki są pewnym uogólnieniem zwykłych tablic, które nie są jednak indeksowane liczbami całkowitymi, a kluczami dowolnego typu. Klucze te są jednak wpierw przepuszczane przez pewną funkcję (funkcję haszującą) i dopiero wynik używany jest do znalezienia wartości, z którą ów klucz jest związany. Istnieje też kilka metod rozwiązywania problemu kolizji, gdy kilka kluczy zostanie odwzorowanych na to samo miejsce w tablicy.
Zaletą haszowania jest szybkość: średnio dostęp do wartości związanej z danym kluczem jest wykonywanym w czasie stałym. Tak, stałym, czyli O(1). Jakkolwiek można być pesymistą i przewidywać, że przy odpowiednio złośliwym doborze kluczy czas ten urośnie do liniowego (O(n), ale w praktyce tablice haszujące zachowują się świetnie. No a to jest przecież najważniejsze :)

W STL nie ma jednak kontenerów używających haszowania. Mimo to wiele kompilatorów oferuje je we własnym zakresie. I tak nasz ulubiony Visual C++ udostępnia dwa "półstandardowe" nagłówki: hash_set i hash_map. Zawierają one klasy (multi)map oraz (multi)zbiorów, z wierzchu wyglądających właściwie identycznie jak zwykłe mapy i zbiory (std::map i std::set) z STL. Klasy te nazywają się hash_set, hash_map, itp., i celem zachowania zgodności ze standardem C++ umieszczone są nie w przestrzeni std, lecz stdext.

O istnieniu tych klas warto pamiętać, gdy tworzymy kod kluczowy pod względem wydajności. W programowaniu gier często są to na przykład przeróżne menedżery zasobów, które odwzorowują pewne identyfikatory (np. łańcuchy znaków) na obiekty w rodzaju modeli, tekstur, próbek dźwiękowych, itd. Szybkość działania przechowujących je pojemników może być ważna i warto poszukać alternatyw dla standardowej klasy map (która przecież teoretycznie "wyciąga" tylko O(logn) dla wyszukiwania). Jeśli przy okazji martwimy się przenośnością na inne kompilatory niż VC++ lub zgodnością ze standardem, to możemy spróbować przełączania między zwykłymi a haszującymi pojemnikami, np. tak:

#if defined(_MSC_VER) && !defined(STANDARD_COMPLIANCE)
    typedef stdext::hash_map<std::string, Object*> ResourceManagerMap;
#else
    typedef std::map<std::string, Object*> ResourceManagerMap;
#endif

Naturalnie 'kod niezależny od kontenera' to dość często utopia, ale przy korzystaniu tylko z prostych operacji typu wstawianie-usuwanie-wyszukiwanie nie powinniśmy napotkać większych problemów.

Tags: , , ,
Author: Xion, posted under Uncategorized » 3 comments

Triki z PowerShellem #4 – DirectX

2008-06-10 11:13

Jakiś czas temu pokazałem, że w PowerShellu można zrobić rzeczy, które są - jak na konsolkę tekstową - co najmniej niespotykane. Przedstawiłem na przykład sposób na wyświetlenie zwykłego windowsowego okienka. Nie było ono aczkolwiek zbyt interesujące, jako że w środku było zupełnie puste - chociaż, oczywiście, Aero robił co mógł, by wyglądało ono na atrakcyjniejsze niż było w rzeczywistości ;)
Jako że zajmowania się rzeczami zupełnie nieprzydatnymi nigdy dość, postanowiłem więc pewnego razu spróbować wnieść w stworzoną formę nieco więcej życia. Efekty okazały się raczej ciekawe, przedstawiając się następująco:

DirectX w PowerShellu - trójkąt (screen 1) DirectX w PowerShellu - trójkąt (screen 2) DirectX w PowerShellu - trójkąt (screen 3)

Tak, to jest trójkąt. Tak, on się obraca. Tak, na pasku tytułowym okna jest napisane Direct3D... Wszystkie te trzy fakty nie są żadnym zbiegiem okoliczności :) Za wszystko odpowiedzialny jest ten ono skrypt:

# D3D.ps1 - próba użycia Direct3D w PowerShellu

# Ładujemy assemblies
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("Microsoft.DirectX") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("Microsoft.DirectX.Direct3D") | Out-Null

# Tworzymy okienko
$form = New-Object Windows.Forms.Form
$form.ControlBox = $false # Bo zamykanie niespecjalnie działa :P
$form.Text = "Direct3D"

# Tworzymy urządzenie
$pp = New-Object Microsoft.DirectX.Direct3D.PresentParameters
$pp.BackBufferCount = 1
$pp.Windowed = $true
$pp.SwapEffect = [Microsoft.DirectX.Direct3D.SwapEffect]::Discard
$pp.DeviceWindow = $form
$pp.PresentationInterval = [Microsoft.DirectX.Direct3D.PresentInterval]::Immediate
$dev = New-Object Microsoft.DirectX.Direct3D.Device @(0,
    [Microsoft.DirectX.Direct3D.DeviceType]::Hardware, $form,
    [Microsoft.DirectX.Direct3D.CreateFlags]::HardwareVertexProcessing, $pp)
   
# Przygotowujemy się do rysowania
$dev.RenderState.Lighting = $false  # Wyłączamy oświetlenie
$tri = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored[] @(3)
$tri[0] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
    0.0, 0.0, 0.5, [Drawing.Color]::Blue.ToArgb())
$tri[1] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
    0.5, -0.5, 0.5, [Drawing.Color]::Red.ToArgb())
$tri[2] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
    -0.5, -0.5, 0.5, [Drawing.Color]::Green.ToArgb())

# Wyświetlamy
[Windows.Forms.Application]::EnableVisualStyles()
$form.Show()
$time = 0
$lastTickCount = [Environment]::TickCount

# Pętlimy się
while ($time -lt 5000) {
    # Przetwarzanie komunikatów
    [Windows.Forms.Application]::DoEvents()
    $time += [Environment]::TickCount - $lastTickCount
    $lastTickCount = [Environment]::TickCount
   
    # Rysujemy
    $dev.Clear([Microsoft.DirectX.Direct3D.ClearFlags]::Target,
        [Drawing.Color]::Lime, [float]1.0, 0)
    $dev.BeginScene()
        # Ustawiamy macierz obrotu
        $angle = (10 * $time / 1000.0) / (2 * [Math]::PI)
        $dev.Transform.World = [Microsoft.DirectX.Matrix]::RotationZ($angle)
       

        # Rysujemy trójkąt
        $dev.VertexFormat = [Microsoft.DirectX.Direct3D.CustomVertex+PositionColored]::Format
        $dev.DrawUserPrimitives([Microsoft.DirectX.Direct3D.PrimitiveType]::TriangleList,
            1, $tri)
    $dev.EndScene()
    $dev.Present()
}

Przyznam, że byłem mocno zaskoczony, iż taki trik jest w ogóle możliwy przy pomocy czegoś, co w założeniu jest tylko powłoką tekstową dla poleceń administracyjnych. A tu proszę: DirectX w całej (aczkolwiek zarządzanej) okazałości!
Nietrudno rzecz jasna zauważyć, że powyższy skrypt jest w dużym stopniu tłumaczeniem na język PowerShella odpowiedniego kodu, który mógłby zostać napisany w dowolnym języku programowania z platformy .NET. Trzeba było jedynie poradzić sobie z pewnymi niedogodnościami, jak na przykład koniecznością ręcznego załadowania assemblies czy jakimś obejściem braku sensownej możliwości reakcji na zdarzenia - tutaj "program" po prostu kończy się po 5 sekundach.

Właśnie ta niemożność sprawia, że tandem PowerShell + DirectX zapewne nie ma przed sobą świetlanej przyszłości w grach :) Przez chwilę zastanawiałem się, czy wobec tego nie da się go użyć do tworzenia dem... Niestety, już powyższy skrypt zajmuje ponad 2 kilobajty, nie potrafiąc przy tym zbyt wiele, więc ta możliwość też prawdopodobnie odpada.
Pozostaje zatem pokazywanie podobnych trików linuksowcom i wytykanie im, że ich bash czegoś takiego nie potrafi ;D

Tags: , ,
Author: Xion, posted under Applications » 13 comments

Nadmuchiwanie obiektów

2008-06-08 15:24

Od kilku dni nadspodziewanie popularnym sportem jest piłka nożna, co zapewne nie jest przypadkowe ;-) A jeśli już chodzi o piłkę, to musi być ona odpowiedniej jakości. I mówię tu o tym niewielkim, prawie okrągłym obiekcie: aby był przydatny, musi być... nadmuchany :) I właśnie o 'nadmuchiwaniu' powiem dzisiaj słów kilka.
Chodzi mi oczywiście o graficzny efekt rozszerzania się obiektu 3D, wyglądający tak, jakby ktoś w ów obiekt pompował powietrze. Można by pomyśleć, że osiągnięcie go jest niezmiernie trudne, bo przecież wchodząca w grę mechanika płynów (ruch gazu "wypełniającego" obiekt) jest jakąś kosmiczną fizykę. Ale - jak to często bywa - przybliżony a wyglądający niemal równie dobrze rezultat możemy dostać o wiele mniejszym wysiłkiem.

Jak? Sztuczka jest bardzo prosta. Aby nasz mesh zaczął się rozciągać na wszystkie strony, należy po prostu odpowiednio poprzesuwać mu wierzchołki. Wspomniane 'wszystkie strony' oznaczają tak naprawdę tyle, że każdy punkt powierzchni obiektu oddala się od niej w kierunku prostopadłym. Kierunek tego ruchu jest wyznaczony po prostu przez normalną.
Efekt jest w istocie trywialny i jego kod w HLSL/Cg może wyglądać na przykład tak:

float fPumpFactor;  // współczynnik nadmuchania obiektu

struct VS_INPUT
{
    float3 Position : POSITION;
    float3 Normal : NORMAL;
    // ...
}

void vs_main(VS_INPUT In, /* ... */)
{
    // nadmuchujemy
    In.Position += fPumpFactor * normalize(In.Normal);

    // (reszta vertex shadera)
}

Wśród zastosowań tego prostego triku można wymienić chociażby wyświetlanie poświaty wokół obiektów. Wystarczy do tego narysować obiekt najpierw w wersji zwykłej, a potem półprzezroczystej i nieco napompowanej.

A tak prezentują się rezultaty, gdy zaczniemy nadmuchiwać poczciwą futbolówkę:

Nadmuchana piłka nożna (PF = -1) Nadmuchana piłka nożna (PF = 2) Nadmuchana piłka nożna (PF = 8)

Na Euro jak znalazł ;]

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

Skróty i linki NTFS

2008-06-07 15:26

Tworzenie skrótu w Windows VistaSystem Windows od niepamiętnych czasów (a dokładniej od wersji 95) pozwala na tworzenie skrótów (shortcut), czyli plików będących odwołaniami do innych plików lub katalogów. Skróty mają zwykle rozszerzenie .lnk i występują głównie w katalogach tworzących strukturę Menu Start.
Wewnętrznie nie ma w nich nic pomysłowego, gdyż są to zwykłe pliki w formacie na wpółbinarnym, zawierające docelową ścieżkę lub URI (np. adres internetowy). Ich obsługa realizowana jest na poziomie Eksploratora Windows, więc nie są one specjalnie funkcjonalne. Otwarcie pliku skrótu w dowolnym programie nie spowoduje na przykład odczytania zawartości docelowego pliku, lecz samego skrótu (który zwykle ma kilkaset bajtów). Podobnie skrót do katalogu Foo o nazwie Bar.lnk nie spowoduje, że do pliku Foo/Coś.txt odwołamy się poprzez Bar.lnk/Coś.txt.

Ilustracja działania hardlinków
Ilustracja działania linków twardych
(Źródło: Wikipedia)

Taką funkcjonalność zapewniają jednak linki twarde i symboliczne, obecne w nowszym wersjach NTFS. Są one bardzo podobne do swoich odpowiedników w systemach plików ext2 i ext3 (uniksowych). I tak:

  • Link twardy (hardlink) to jakby alternatywna nazwa dla pliku, czyli nowa pozycja systemu plików odwołująca się do tych samych danych. Używając którejkolwiek z nich, można zmienić zawartość pliku i modyfikacje te będące widoczne dla innych, nawet jeśli uzyskają oni dostęp do pliku za pomocą innych jego nazw.
    W Windows 2000 i nowszych linki twarde można tworzyć poleceniem fsutil hardlink create link cel, a w Windows Vista dodatkowo mklink /H link cel. Programowo robi się funkcją WinAPI o nazwie CreateHardLink.
  • Link symboliczny (symlink) jest z kolei łączem do istniejącej nazwy pliku lub katalogu; zamiast do faktycznych danych na dysku odwołuje się więc do innego elementu systemu plików. To właściwie taki skrót pozbawiony ograniczeń, o których wspominałem na początku. Możliwe jest jednak, aby programy operowały bezpośrednio na linkach symbolicznych, o ile odpowiednio ładnie o to poproszą :)
    W miarę porządna implementacja obsługi symlinków jest niestety zawarta dopiero w Windows Vista; można je tworzyć programem mklink (trzeba jednak zaznaczać jawnie, czy robimy link do pliku czy do katalogu). We wcześniejszych wersjach możliwe jest jedynie tworzenie NTFS junctions, będących "linkami" tylko do katalogów oraz obarczonych innymi ograniczeniami (na przykład Eksplorator w Windows XP dość słabo sobie z nimi radzi).

Jak nietrudno zauważyć, podstawową zaletą linków nad zwykłymi skrótami jest to, że te pierwsze są obsługiwane na poziomie systemu plików. Wobec tego działają one w dowolnym programie i zupełnie przezroczyście dla aplikacji. Trochę szkoda, że ten przydatny drobiazg pochodzący z systemów uniksowych dopiero teraz pojawia się w Windows... ale przecież lepiej późno niż wcale ;]

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


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