Czytanie kodu. Punkt widzenia twórców oprogramowania open source

Książka "Czytanie kodu. Punkt widzenia twórców oprogramowania" open source to pierwszy na rynku podręcznik poś

919 126 13MB

Polish Pages [432] Year 2004

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Czytanie kodu. Punkt widzenia twórców oprogramowania open source

Table of contents :
Przedmowa (7)
Wstęp (11)
Rozdział 1. Wprowadzenie (15)
1.1. Motywy i metody czytania kodu (16)
1.1.1. Kod jako literatura (16)
1.1.2. Kod jako model (19)
1.1.3. Utrzymanie kodu (20)
1.1.4. Rozwój (21)
1.1.5. Ponowne wykorzystanie (22)
1.1.6. Inspekcje (23)
1.2. Jak czytać tę książkę? (24)
1.2.1. Konwencje typograficzne (24)
1.2.2. Diagramy (25)
1.2.3. Ćwiczenia (27)
1.2.4. Materiał dodatkowy (27)
1.2.5. Narzędzia (28)
1.2.6. Zarys treści (28)
1.2.7. Debata na temat najlepszego języka (29)
Dalsza lektura (30)
Rozdział 2. Podstawowe konstrukcje programistyczne (33)
2.1. Pełny program (33)
2.2. Funkcje i zmienne globalne (39)
2.3. Pętle while, instrukcje warunkowe i bloki (42)
2.4. Instrukcja switch (45)
2.5. Pętle for (47)
2.6. Instrukcje break i continue (50)
2.7. Wyrażenia znakowe i logiczne (52)
2.8. Instrukcja goto (55)
2.9. Refaktoryzacja w skrócie (57)
2.10. Pętle do i wyrażenia całkowite (63)
2.11. Podsumowanie wiadomości o strukturach sterujących (65)
Dalsza lektura (71)
Rozdział 3. Zaawansowane typy danych języka C (73)
3.1. Wskaźniki (73)
3.1.1. Powiązane struktury danych (74)
3.1.2. Dynamiczne przydzielanie struktur danych (74)
3.1.3. Wywołania przez referencję (75)
3.1.4. Uzyskiwanie dostępu do elementów danych (76)
3.1.5. Tablice jako argumenty i wyniki (77)
3.1.6. Wskaźniki na funkcje (78)
3.1.7. Wskaźniki jako aliasy (81)
3.1.8. Wskaźniki a ciągi znaków (82)
3.1.9. Bezpośredni dostęp do pamięci (84)
3.2. Struktury (85)
3.2.1. Grupowanie elementów danych (85)
3.2.2. Zwracanie wielu elementów danych z funkcji (85)
3.2.3. Odwzorowanie organizacji danych (86)
3.2.4. Programowanie obiektowe (87)
3.3. Unie (89)
3.3.1. Wydajne wykorzystanie pamięci (89)
3.3.2. Implementacja polimorfizmu (90)
3.3.3. Uzyskiwanie dostępu do różnych reprezentacji wewnętrznych (91)
3.4. Dynamiczne przydzielanie pamięci (92)
3.4.1. Zarządzanie wolną pamięcią (95)
3.4.2. Struktury z dynamicznie przydzielanymi tablicami (97)
3.5. Deklaracje typedef (98)
Dalsza lektura (100)
Rozdział 4. Struktury danych języka C (101)
4.1. Wektory (102)
4.2. Macierze i tabele (106)
4.3. Stosy (110)
4.4. Kolejki (112)
4.5. Mapy (114)
4.5.1. Tablice mieszające (117)
4.6. Zbiory (119)
4.7. Listy (120)
4.8. Drzewa (127)
4.9. Grafy (131)
4.9.1. Przechowywanie wierzchołków (132)
4.9.2. Reprezentacje krawędzi (133)
4.9.3. Przechowywanie krawędzi (136)
4.9.4. Właściwości grafu (137)
4.9.5. Struktury ukryte (138)
4.9.6. Inne reprezentacje (138)
Dalsza lektura (139)
Rozdział 5. Zaawansowane techniki sterowania przebiegiem programów (141)
5.1. Rekurencja (141)
5.2. Wyjątki (147)
5.3. Równoległość (151)
5.3.1. Równoległość sprzętowa i programowa (151)
5.3.2. Modele sterowania (153)
5.3.3. Implementacja wątków (158)
5.4. Sygnały (161)
5.5. Skoki nielokalne (165)
5.6. Podstawienie makr (167)
Dalsza lektura (171)
Rozdział 6. Metody analizy dużych projektów (173)
6.1. Techniki projektowe i implementacyjne (173)
6.2. Organizacja projektu (175)
6.3. Proces budowy i pliki Makefile (183)
6.4. Konfiguracja (190)
6.5. Kontrola wersji (194)
6.6. Narzędzia związane z projektem (201)
6.7. Testowanie (205)
Dalsza lektura (212)
Rozdział 7. Standardy i konwencje pisania kodu (213)
7.1. Nazwy plików i ich organizacja wewnętrzna (214)
7.2. Wcięcia (216)
7.3. Formatowanie (218)
7.4. Konwencje nazewnictwa (221)
7.5. Praktyki programistyczne (224)
7.6. Standardy związane z procesem rozwojowym (226)
Dalsza lektura (227)
Rozdział 8. Dokumentacja (229)
8.1. Rodzaje dokumentacji (229)
8.2. Czytanie dokumentacji (230)
8.3. Problemy dotyczące dokumentacji (241)
8.4. Dodatkowe źródła dokumentacji (242)
8.5. Popularne formaty dokumentacji w środowisku open-source (246)
Dalsza lektura (251)
Rozdział 9. Architektura (253)
9.1. Struktury systemów (253)
9.1.1. Podejście scentralizowanego repozytorium i rozproszone (254)
9.1.2. Architektura przepływu danych (259)
9.1.3. Struktury obiektowe (261)
9.1.4. Architektury warstwowe (264)
9.1.5. Hierarchie (266)
9.1.6. Przecinanie (267)
9.2. Modele sterowania (269)
9.2.1. Systemy sterowane zdarzeniami (269)
9.2.2. Menedżer systemowy (273)
9.2.3. Przejścia stanów (274)
9.3. Pakietowanie elementów (277)
9.3.1. Moduły (277)
9.3.2. Przestrzenie nazw (279)
9.3.3. Obiekty (283)
9.3.4. Implementacje ogólne (295)
9.3.5. Abstrakcyjne typy danych (298)
9.3.6. Biblioteki (299)
9.3.7. Procesy i filtry (302)
9.3.8. Komponenty (304)
9.3.9. Repozytoria danych (306)
9.4. Wielokrotne użycie architektury (308)
9.4.1. Schematy strukturalne (308)
9.4.2. Generatory kodu (309)
9.4.3. Wzorce projektowe (310)
9.4.4. Architektury dziedzinowe (312)
Dalsza lektura (316)
Rozdział 10. Narzędzia pomocne w czytaniu kodu (319)
10.1. Wyrażenia regularne (320)
10.2. Edytor jako przeglądarka kodu (323)
10.3. Przeszukiwanie kodu za pomocą narzędzia grep (326)
10.4. Znajdowanie różnic między plikami (333)
10.5. Własne narzędzia (335)
10.6. Kompilator jako narzędzie pomocne w czytaniu kodu (338)
10.7. Przeglądarki i upiększacze kodu (342)
10.8. Narzędzia używane w czasie uruchomienia (347)
10.9. Narzędzia nieprogramowe (351)
Dostępność narzędzi oraz dalsza lektura (353)
Rozdział 11. Pełny przykład (355)
11.1. Przegląd (355)
11.2. Plan działania (356)
11.3. Wielokrotne użycie kodu (358)
11.4. Testowanie i uruchamianie (363)
11.5. Dokumentacja (368)
11.6. Uwagi (369)
Dodatek A Struktura dołączonego kodu (371)
Dodatek B Podziękowania dla autorów kodu źródłowego (375)
Dodatek C Pliki źródłowe (377)
Dodatek D Licencje kodu źródłowego (385)
D.1. ACE (385)
D.2. Apache (386)
D.3. ArgoUML (387)
D.4. DemoGL (388)
D.5. hsqldb (388)
D.6. NetBSD (389)
D.7. OpenCL (390)
D.8. Perl (390)
D.9. qtchat (393)
D.10. socket (393)
D.11. vcf (393)
D.12. X Window System (394)
Dodatek E Porady dotyczące czytania kodu (395)
Bibliografia (413)
Skorowidz (427)

Citation preview

Punkt w id z e n ia tw ó rc ó w o p ro g ra m o w a n ia o p e n s o u rc e O o rm d iQ

S c x rt+ tl» «

4*681 LrJ w

darmowe ebooki aktualne czasopisma

A ft 'J j

etiookgiqs.com

Spis treści Przedmowa......................................................................................................... 7 W s tę p ............................................................................................................... 11 Rozdział 1. W prowadzenie................................................................................................. 15

1.1. Motywy i metody czytania kodu.......................................................................... 16 1.1.1. Kod jako literatura..................................................................................... 16 1.1.2. Kod jako model.........................................................................................19 1.1.3. Utrzymanie kodu...................................................................................... 20 1.1.4. Rozwój.....................................................................................................21 1.1.5. Ponowne wykorzystanie............................................................................22 1.1.6. Inspekcje.................................................................................................. 23 1.2. Jak czytać tę książkę?......................................................................................... 24 1.2.1. Konwencje typograficzne..........................................................................24 1.2.2. Diagramy.................................................................................................. 25 1.2.3. Ćwiczenia................................................................................................. 27 1.2.4. Materiał dodatkowy........................................................ 27 1.2.5. Narzędzia.................................................................................................. 28 1.2.6. Zarys treści............................................................................................... 28 1.2.7. Debata na temat najlepszego języka.......................................................... 29 Dalsza lektura............................................................................................................ 30 Rozdział 2. Podstawowe konstrukcje program istyczne................................................ 33

2.1. Pełny program..................................................................................................... 33 2.2. Funkcje i zmienne globalne.................................................................................39 2.3. Pętle while, instrukcje warunkowe i bloki.......................................................... 42 2.4. Instrukcja switch................................................................................................. 45 2.5. Pętle for...............................................................................................................47 2.6. Instrukcje break i continue...................................................................................50 2.7. Wyrażenia znakowe i logiczne............................................................................. 52 2.8. Instrukcja goto.................................................................................................... 55 2.9. Refaktoryzacja w skrócie.....................................................................................57 2.10. Pętle do i wyrażenia całkowite.............................................................................63 2.11. Podsumowanie wiadomości o strukturach sterujących......................................... 65 Dalsza lektura............................................................................................................ 71 Rozdział 3. Zaawansowane typy danych języka C ........................................................ 73

3.1.

Wskaźniki........................................................................................................ 73 3.1.1. Powiązane struktury danych...................... 74 3.1.2. Dynamiczne przydzielanie struktur danych................................................74

4

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

3.1.3. Wywołania przez referencję.................................... 75 3.1.4. Uzyskiwanie dostępu do elementów danych.............................................. 76 3.1.5. Tablice jako argumenty i wyniki............................................................... 77 3.1.6. Wskaźniki na funkcje................................................................................78 3.1.7. Wskaźniki jako aliasy................................................................................81 3.1.8. Wskaźniki a ciągi znaków......................................................................... 82 3.1.9. Bezpośredni dostęp do pamięci................................................................. 84 3.2. Struktury.............................................................................................................85 3.2.1. Grupowanie elementów danych................................................................ 85 3.2.2. Zwracanie wielu elementów danych z funkcji............................................ 85 3.2.3. Odwzorowanie organizacji danych............................................................ 86 3.2.4. Programowanie obiektowe........................................................................ 87 3.3. Unie....................................................................................................................89 3.3.1. Wydajne wykorzystanie pamięci............................................................... 89 3.3.2. Implementacja polimorfizmu..................................................................... 90 3.3.3. Uzyskiwanie dostępu do różnych reprezentacji wewnętrznych...................91 3.4. Dynamiczne przydzielanie pamięci..................................................................... 92 3.4.1. Zarządzanie wolną pamięcią...................................................................... 95 3.4.2. Struktury' z dynamicznie przydzielanymi tablicami.................................... 97 3.5. Deklaracje typcdef.............................................................................................. 98 Dalsza lektura...........................................................................................................100 Rozdział 4.

Struktury danych języka C.............................................................................. 101 4.1. Wektory............................................................................................................ 102 4.2. Macierze i tabele................................................................................................106 4.3. Stosy................................................................................................................110 4.4. Kolejki.............................................................................................................. 112 4.5. Mapy................................................................................................................ 114 4.5.1. Tablice mieszające................................................................................... 117 4.6. Zbiory............................................................................................................... 119 4.7. Listy.................................................................................................................. 120 4.8. Drzewa.............................................................................................................. 127 4.9. Grafy................................................................................................................. 131 4.9.1. Przechowywanie wierzchołków............................................................... 132 4.9.2. Reprezentacje krawędzi............................................................................133 4.9.3. Przechowywanie krawędzi.......................................................................136 4.9.4. Właściwości grafu................................................................................... 137 4.9.5. Struktury ukryte..................................................................................... 138 4.9.6. Inne reprezentacje...................................................................................138 Dalsza lektura...........................................................................................................139

Rozdział 5.

Z aaw ansow ane techniki sterow ania przebiegiem program ów 141 5.1. Rekurencja........................................................................................................ 141 5.2. Wyjątki............................................................................................................. 147 5.3. Równoległość....................................................................................................151 5.3.1. Równoległość sprzętowa i programowa................................................... 151 5.3.2. Modele sterowania.................................................................................153 5.3.3. Implementacja wątków.............................................................................158 5.4. Sygnały............................................................................................................. 161 5.5. Skoki nielokalne................................................................................................165 5.6. Podstawienie makr............................................................................................. 167 Dalsza lektura...................................... 171

Spis treści

Rozdział 6.

5

M etody analizy dużych p ro je k tó w .............................................................173

6.1. Techniki projektowe i implementacyjne.............................................................173 6.2. Organizacja projektu.......................................................................................... 175 6.3. Proces budowy i pliki Makefile......................................................................... 183 6.4. Konfiguracja......................................................................................................190 6.5. Kontrola wersji..................................................................................................194 6.6. Narzędzia związane z projektem........................................................................201 6.7. Testowanie.................................................................................... 205 Dalsza lektura.......................................................................................................... 212 Rozdział 7. Standardy i konwencje pisania kodu........................................................213

7.1. Nazwy plików i ich organizacja wewnętrzna......................................................214 7.2. Wcięcia.............................................................................................................216 7.3. Formatowanie................................................................................................... 218 7.4. Konwencje nazewnictwa....................................................................................221 7.5. Praktyki programistyczne..................................................................................224 7.6. Standardy związane z procesem rozwojowym................................. 226 Dalsza lektura.......................................................................................................... 227 Rozdział 8.

D okum entacja.............................................................................................. 229

8.1. Rodzaje dokumentacji........................................................................................229 8.2. Czytanie dokumentacji.......................................................................................230 8.3. Problemy dotyczące dokumentacji.................................................................... 241 8.4. Dodatkowe źródła dokumentacji........................................................................ 242 8.5. Popularne formaty dokumentacji w środowisku open-source..............................246 Dalsza lektura.......................................................................................................... 251 Rozdział 9. A rc h ite k tu ra .................................................................................................. 253

9.1. Struktury systemów.......................................................................................... 253 9.1.1. Podejście scentralizowanego repozytorium i rozproszone.........................254 9.1.2. Architektura przepływu danych............................................................... 259 9.1.3. Struktury obiektowe................................................................................261 9.1.4. Architektury warstwowe..........................................................................264 9.1.5. Hierarchie............................................................................................... 266 9.1.6. Przecinanie..............................................................................................267 9.2. Modele sterowania............................................................................................ 269 9.2.1. Systemy sterowane zdarzeniami.............................................................. 269 9.2.2. Menedżer systemowy..............................................................................273 9.2.3. Przejścia stanów......................................................................................274 9.3. Pakietowanie elementów....................................................................................277 9.3.1. Moduły................................................................................................... 277 9.3.2. Przestrzenie nazw....................................................................................279 9.3.3. Obiekty................................................................................................... 283 9.3.4. Implementacje ogólne............................................................................. 295 9.3.5. Abstrakcyjne typy danych....................................................................... 298 9.3.6. Biblioteki................................................................................................ 299 9.3.7. Procesy i filtry.........................................................................................302 9.3.8. Komponenty'........................................................................................... 304 9.3.9. Repozytoria danych.................................................................................306 9.4. Wielokrotne użycie architektury........................................................................ 308 9.4.1. Schematy strukturalne............................................................................. 308 9.4.2. Generatory kodu......................................................................................309 9.4.3. Wzorce projektowe..................................................................................310 9.4.4. Architektury dziedzinowe........................................................................312 Dalsza lektura.......................................................................................................... 316

6

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rozdział 10. Narzędzia pomocne w czytaniu kodu........................................................319

10.1. Wyrażenia regularne....................................................................................... 320 10.2. Edytor jako przeglądarka kodu........................................................................323 10.3. Przeszukiwanie kodu za pomocą narzędzia grep..............................................326 10.4. Znajdowanie różnic między plikami................................................................ 333 10.5. Własne narzędzia............................................ 335 10.6. Kompilator jako narzędzie pomocne w czytaniu kodu..................................... 338 10.7. Przeglądarki i upiększacze kodu...................................................................... 342 10.8. Narzędzia używane w czasie uruchomienia......................................................347 10.9. Narzędzia nieprogramowe...............................................................................351 Dostępność narzędzi oraz dalsza lektura................................................................... 353 355 11.1. Przegląd............................................. 11.2. Plan działania..................................... ............................................................356 11.3. Wielokrotne użycie kodu................... 11.4. Testowanie i uruchamianie................ ............................................................363 11.5. Dokumentacja.................................... ........................................................... 368 369 11.6. Uwagi............................... Dodatek A

Struktura dołączonego kodu..................................................................... 371

Dodatek B

Podziękowania dla autorów kodu źródłowego........................................375

Dodatek C

Pliki źródłow e...............................................................................................377

Dodatek D

Licencje kodu źródłow ego......................................................................... 385

D.l. ACE.................................................................................................................385 386 D.2. Apache................................................. D.3. ArgoUML........................................................................................................ 387 D.4. DcmoGL.......................................................................................................... 388 D.5. hsqldb...............................................................................................................388 D.6. NetBSD............................................................................................................ 389 D.7. OpenCL............................................................................................................ 390 D.8. Perl...................................................................................................................390 D.9. qtchal...............................................................................................................393 D. 10. socket........................................... 393 D. 11. vcf........................................................................ 393 D. 12. X Window System........................................................................................... 394 Dodatek E

Porady dotyczące czytania kodu...............................................................395 B ibliog ra fia.................................................................................................... 413 Skorowidz...................................................................................................... 427

Przedmowa Jesteśmy programistami. Nasza praca (a w wielu przypadkach równocześnie pasja) polega na kreowaniu określonych produktów poprzez pisanie kodu. Nie spełniamy wymagań naszych klientów dzięki bogatym zbiorom diagramów, szczegółowym har­ monogramom projektów ani stertom dokumentacji projektowej. Wszystko to są po­ bożne życzenia, wyrażające to, co chcielibyśmy, aby było prawdą. W rzeczywistości wypełniamy swoje obowiązki pisząc kod: to właśnie kod jest tym, co realne. Tego nas nauczono i wydaje się to rozsądne. Nasza praca polega na pisaniu kodu, więc musimy się nauczyć, w jaki sposób należy go zapisywać. Na studiach uczy się pisania programów, na szkoleniach omawia się sposoby pisania kodu zgodnego z nowymi bi­ bliotekami i interfejsami AP1. Okazuje się jednak, że jest to jedna z największych bolą­ czek środowisk przemysłowych. Wynika to z faktu, że nauka pisania poprawnego kodu powinna opierać się na umiejęt­ ności czytania kodu, ogromnych ilości kodu. Kodu o wysokiej i niskiej jakości, kodu pisanego w asemblerze, kodu pisanego w języku Haskell, kodu pisanego przez obce osoby pracujące tysiące kilometrów od nas, wreszcie kodu pisanego przez nas samych tydzień wcześniej. O ile się tego nie zapewni, jest się skazanym na nieustanne odkry­ wanie na nowo tego, co zostało już określone, powtarzanie zarówno sukcesów, jak i porażek, które miały już miejsce. Warto zadać sobie pytanie, ilu sławnych pisarzy nigdy nie przeczytało dzieła jakiegokol­ wiek innego autora, ilu sławnych malarzy nie analizowało technik malarskich innych artystów, ilu wykwalifikowanych chirurgów nigdy nie kształciło się, podglądając pra­ cę bardziej doświadczonych kolegów, ilu kapitanów pilotujących wielkie samoloty pa­ sażerskie nie latało początkowo w charakterze drugiego pilota, obserwując w realnych warunkach działania kolegi. A jednak tak właśnie wyglądają zadania stawiane programistom: „w bieżącym tygodniu zadanie polega na napisaniu...”. Programiści są uczeni reguł składni i konstruowania wyrażeń, a później oczekuje się od nich, że będą w stanie pisać oprogramowanie rów­ noważne największym dziełom literatury.

8

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Ironia polega na tym. że nigdy wcześniej nie było lepszych czasów na czytanie kodu. Dzięki ogromnemu udziałowi społeczności programistów tworzących kod typu open-source obecnie posiadamy dostęp do gigabajtów kodu źródłowego przesyłanego internetem i czekającego na przeczytanie. Wybrawszy dowolny język programowania, dowol­ ną problematykę. dowolny poziom, od mikrokodu do wysokopoziomowych funkcji biz­ nesowych, zawsze można znaleźć adekwatny kod źródłowy i dokładnie mu się przyjrzeć. Czytanie kodu może sprawiać wiele przyjemności. Piszący niniejsze słowa uwielbia czy­ tać kod autorstwa innych osób. Taka lektura służy nauce stosowania sztuczek i wyłapy­ wania pułapek programistycznych. Niekiedy można znaleźć niewielkie, ale wartościo­ we perełki. Wystarczy chociażby wspomnieć o przyjemności poznania podprogramu konwertującego liczby zapisane w postaci dwójkowej do postaci ósemkowej utwo­ rzonego w asemblerze komputera PDP-11. Umożliwiał on wyświetlenie sześciu cyfr ósemkowych dzięki niewielkiej pętli, niezawierającej licznika. Niekiedy kod czyta się dla samej lektury, niczym książkę, którą kupujemy na dworcu przed długą podróżą. Oczekujemy wówczas od autora, że uda mu się nas zaciekawić przemyślaną akcją i niespodziewanymi symetriami. Program gpic autorstwa Jamesa Clarka (należy on do pakietu GNU grojf) stanowi doskonały przykład tego rodzaju kodu. Reprezentuje implementację czegoś, co pozornie wydaje się bardzo skomplikowane (deklaratywny, niezależny od urządzenia język służący do rysowania obrazów) w for­ mie spójnej i eleganckiej struktury. Tego rodzaju doświadczenia mogą zainspirować programistę do podejmowania prób konstruowania własnego kodu w równie eleganc­ ki sposób. Czasem kod czyta się z nieco większą dozą krytycyzmu. Taka lektura postępuje w wol­ niejszym tempie. W trakcie czytania zadaje się sobie pytania w rodzaju: „Dlaczego za­ pisano to właśnie w ten sposób?” lub „Jakie doświadczenia skłoniły autora do wyboru właśnie tego rozwiązania?”. Dzieje się tak, kiedy przegląda się kod w poszukiwaniu problemów'. Wyszukuje się wzorce i wskazówki, które mogą okazać się pomocne. Jeżeli można stwierdzić, że w pewnym fragmencie kodu autorowi nie udało się założyć blo­ kady na współdzieloną strukturę danych, można podejrzewać, że podobna sytuacja wy­ stąpi gdzie indziej, a następnie zastanowić się, czy taki błąd mógłby być odpow iedzialny za problem, który napotkaliśmy. Takie znajdowane niedociągnięcia mogą również być wykorzystywane jako forma sprawdzenia zrozumienia danej problematyki. Często zdarza się, że to. co wydaje się być problemem, po bliższym zbadaniu okazuje się cał­ kowicie poprawnym kodem. Tego rodzaju sytuacje pozwalają wiele się nauczyć. W rzeczywistości czytanie kodu stanowi jeden z najskuteczniejszych sposobów elimi­ nowania problemów występujących w programach. Robert Glass. jeden z korektorów niniejszej książki, stwierdził: „używając poprawnie metod badania kodu można usunąć ponad 90 procent błędów z produktu programistycznego, zanim przejdzie on do fazy testów1”. W tym samym artykule cytuje on wyniki badań dowodzące, że: „kontrole­ rzy badający kod znajdowali o 90 procent więcej błędów od kontrolerów badających procesy”. Co interesujące, piszący te słowa, czytając fragmenty kodu zawarte w niniej­ szej książce znalazł kilka błędów oraz. technik kodowania o wątpliwej wartości. Są to problemy dotyczące kodu, występujące w dziesiątkach tysięcy miejsc na świecie. Żaden 1 http://www.stictyminds.com/se/S2587.asp

Przedmowa

9

z nich nie jest z gruntu problemem o znaczeniu krytycznym, jednak praktyka pokazuje, że zawsze istnieje możliwość poprawienia pisanego kodu. Umiejętność czytania kodu daje bez wątpienia wymierne korzyści. Wie o tym każdy, kto kiedykolwiek współpra­ cował przy recenzowaniu kodu z osobami nie potrafiącymi go czytać. Nie można również zapomnieć o konserwacji — tej mniej przyjemnej stronie procesu rozwijania kodu. Nie istnieją adekwatne statystyki, jednak większość badaczy zgadza się. że ponad połowa czasu związanego z programowaniem faktycznie dotyczy prze­ glądania już istniejącego kodu: zwiększanie funkcjonalności, usuwanie błędów, inte­ gracja z nowymi środowiskami i tak dalej. Umiejętność czytania kodu ma podstawowe znaczenie. Załóżmy, że w programie liczącym 100 000 wierszy istnieje błąd i mamy godzinę na jego znalezienie. Od czego zacząć? Jak stwierdzić, czego naprawdę się szu­ ka? Wreszcie, w jaki sposób określić wpływ zmian, które zamierza się w prowadzić? Ze względu na wszystkie wymienione powody i wiele innych, niniejsza książka z pew­ nością jest pozycją godną uwagi. W swojej istocie jest pragmatyczna — zamiast zasto­ sowania abstrakcyjnego podejścia akademickiego, jej Autor skupił się na kodzie jako takim. Zawarto tu analizy setek fragmentów' kodu, opisano przeróżne sztuczki, pułap­ ki oraz (co równie ważne) idiomy. Jest w niej mowa o kodzie postrzeganym z punktu widzenia jego własnego środowiska oraz o tym, jaki wpływ na kod wywiera środowi­ sko. Przedstawiono również narzędzia przydatne do czytania kodu — od powszechnie używanych, takich jak grep lub find, po o wiele bardziej wyspecjalizowane. Podkreślo­ no tu również wagę procesu tworzenia narzędzi — pisanie kodu pomaga w nauce jego czytania. W końcu, skoro mowa o pragmatyzmie, do książki dołączono całość kodu, który jest w niej omawiany. Dla wygody czytelnika jest on opatrzony odsyłaczami do zawartości płyty CD-ROM. Niniejsza książka powinna zostać uwzględniona w programie każdego szkolenia z za­ kresu programowania i znaleźć się na półce każdego programisty. Jeżeli jako społecz­ ność programistów zaczniemy przywiązywać większą wagę do sztuki czytania kodu. zaoszczędzimy sobie zarówno czasu, jak i nieprzyjemności, zaoszczędzimy pieniądze swoich firm i wreszcie obcowanie z kodem zacznie nam sprawiać jeszcze większą przyjemność. Dave Thomas The Pragmatic Programmers, LLC http://www.pragmaticprogranimer.com/

Wstęp Czytanie kodu to prawdopodobnie jedna z czynności, jakie zawodowy programista wyko­ nuje najczęściej, jednak umiejętności tej bardzo rzadko uczy się w formie przedmiotu zajęć lub formalnie używa jako metody nauczania technik projektowania i programowania. Jednym z powodów istnienia tej nieciekawej sytuacji jest prawdopodobnie fakt, że nie­ gdyś niezmiernie trudno było uzyskać dostęp do rzeczywistego kodu o wysokiej jakości, który można by czytać. Firmy często chroniły kod źródłowy jako tajemnicę handlową i rzadko pozwalały postronnym osobom na jego czytanie, komentowanie, eksperymen­ towanie z nim, a wreszcie uczenie się na jego podstawie. W nielicznych przypadkach, gdy istotny, prawnie zastrzeżony kod mógł wydostać się poza przedsiębiorstwo, wywo­ ływał ogromne zainteresowanie oraz przynosił kreatywne ulepszenia. Jako przykład można przytoczyć fakt. że całe pokolenie programistów ogromnie skorzystało na pu­ blikacji Commentary on the Unix Operating System Johna Lionsa, w której zawarł on opatrzony komentarzami pełny kod źródłowy szóstej edycji jądra systemu Unix. Choć książka Lionsa została napisana za przyzwoleniem firmy AT&T do użytku w trakcie szkoleń dotyczących obsługi systemu i nie była dostępna dla ogółu, jej kopie przez długie lata krążyły w formie odbitek ksero. Jednak w ciągu ostatnich kilku lat popularność oprogramowania typu open-source umoż­ liwiła dostęp do ogromnych ilości kodu. który każdy może bez przeszkód czytać. Nie­ które z najbardziej popularnych obecnie systemów programistycznych, takie jak serwer WWW Apache, język Perl, system operacyjny GNU/Linux. serwer nazw BIND lub serwer poczty sendmail, są dostępne w formie kodu open-source. Autor mial to szczę­ ście. że mógł wykorzystać oprogramowanie open-source w celu napisania niniejszej książki jako elementarza i podręcznika poświęconego kodowi programów. Za cel po­ stawił sobie zaprezentowanie niezbędnych wiadomości i technik czytania kodu pisa­ nego przez innych programistów. Wykorzystując rzeczywiste przykłady pochodzące z funkcjonujących projektów open-source. starał się opisać większość pojęć związanych z kodem, które bez wątpienia są znane każdemu, kto opracowuje oprogramowanie: kon­ strukcje programistyczne, typy danych, struktury danych, przebieg sterowania, organi­ zacja projektu, standardy kodowania, dokumentacja oraz architektura oprogramowania. Inna pozycja książkowa, związana z niniejszą, będzie poświęcona interfejsom oraz kodowi aplikacyjnemu, w tym kwestiom dotyczącym internacjonalizacji i przenośności.

12

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

powszechnie używanym bibliotekom i systemom operacyjnym, kodowi niskopoziomowemu, językom zorientowanym problemowo i deklaratywnym, językom skryptowym oraz systemom wykorzystującym wiele różnych języków. Niniejsza książka to, o ile Autorowi wiadomo, pierwsza pozycja na rynku poświęcona wyłącznie czytaniu kodu jako odrębnej dyscyplinie, wartościowej samej w sobie. Jako taka, bez wątpienia zawiera wiele niedociągnięć; można wskazać o wiele lepsze sposo­ by opisania niektórych z poruszanych zagadnień oraz wymienić kwestie, które pomi­ nięto. Jednakże Autor wierzy, że umiejętność czytania kodu powinna być zarówno na­ uczana w sposób prawidłowy, jak również wykorzystywana jako metoda doskonalenia własnych umiejętności programistycznych. Dlatego też ma nadzieję, że niniejsza książ­ ka sprawi, że w programach nauczania informatyki pojawią się wykłady i ćwiczenia poświęcone czytaniu kodu i za kilka lat studenci będą się uczyć na podstawie obecnie funkcjonujących systemów open-source, podobnie jak ich koledzy z kierunków huma­ nistycznych uczą się języka na podstawie dzieł literackich.

M ateriały dodatkowe Wiele przykładów kodu źródłowego zawartych w niniejszej książce pochodzi z dystry­ bucji źródłowej systemu NetBSD. NetBSD to darmowy, przenośny, uniksowy system operacyjny dostępny na wielu platformach — od 64-bitowych maszyn AlphaServer po urządzenia przenośne. Jego przejrzysty projekt oraz zaawansowane funkcje sprawia­ ją. że stanowi on doskonały wybór zarówno w przypadku środowisk produkcyjnych, jak i badawczych. Autor zdecydował się na wybór NetBSD zamiast innego spośród równie cenionych i popularnych systemów uniksowych (takich jak GNU/Linux, FreeBSD czy OpenBSD) ze względu na fakt, że podstawowym celem projektu NetBSD jest podkreślenie istotności poprawnego projektu oraz dobrze zapisanego kodu. co spra­ wia. że stanowi on doskonały wybór w przypadku chęci przedstawienia przykładów kodu źródłowego. Według opinii jego twórców, niektóre systemy są tworzone zgod­ nie ze strategią .jeśli działa, wszystko jest w porządku", natomiast NetBSD można by opisać słowami „nie działa, dopóki wszystko nie jest w porządku". Ponadto pewne inne cele projektu NetBSD ściśle współgrają z celami niniejszej książki. W szczególności, projekt NetBSD nie zawiera ograniczającej licencji, stanowi przenośny system działają­ cy na wielu platformach sprzętowych, dobrze współpracuje z innymi systemami i jest zgodny ze standardami systemów otwartych na tyle. na ile jest to praktyczne. Kod wy­ korzystywany w niniejszej książce to (obecnie historyczna) wersja export, 19980407. Pewne przykłady stanowią odwołania do błędów, które Autor zdołał zlokalizować w kodzie. Oczywiście, kod projektu NetBSD podlega ciągłej ewolucji i zaprezentowa­ nie przykładów pochodzących z jego nowszej wersji oznaczałoby podjęcie ryzyka, że owe z życia wzięte próbki kodu zostały poprawione. Pozostałe systemy wykorzystywane w niniejszej książce jako źródła przykładów były wybierane z podobnych względów: jakości kodu, struktury, projektu, przydatności, popularności oraz rozprowadzania na licencjach nic niosących ze sobą żadnych proble­ mów w zakresie prezentacji przykładów kodu. Autor starał się zachować równowagę w zakresie doboru używanych języków programowania, wyszukując odpowiedni kod

w językach Java i C++. Jednak w przypadkach, gdy podobne pojęcia można było za­ prezentować przy użyciu różnych języków, wybór padał na język C, traktowany jako wspólny mianownik. Niekiedy Autor wykorzystywał przykłady kodu w celu zilustrowania praktyk kodowa­ nia. które są niebezpieczne, nieprzenośne. nieczytelne lub z jeszcze innych względów godne napiętnowania. Oczywiście, może zostać obwiniony o dyskredytowanie kodu, który został udostępniony przez swoich autorów w dobrej wierze w celu wzmocnienia siły ruchu open source i umożliwienia innym wprowadzenia do niego poprawek, a nie narażenia się na słowa krytyki. Autor z góry pragnie szczerze przeprosić osoby, które mogłyby poczuć się dotknięte pewnymi komentarzami. Na swoją obronę może stwier­ dzić, że w większości przypadków komentarze te nie dotyczą konkretnych fragmentów kodu, ale raczej służą zilustrowaniu prakty k, których należy unikać. Częstokroć taki kod to niejako ofiara swoich czasów, gdyż został napisany w momencie, gdy ograniczenia technologiczne lub innego rodzaju uzasadniały zastosowanie danej techniki kodowania, czasem też dana technika kodowania jest poddawana krytyce w oderwaniu od orygi­ nalnego kontekstu. W każdym razie Autor ma nadzieję, że zawarte w książce komenta­ rze zostaną odebrane z dużą dozą humoru i może szczerze stwierdzić, że jego własny kod zawiera podobne, a czasem zapewne nawet gorsze, zaniedbania.

Podziękowania Ogromna rzesza osób przyczyniła się do powstania niniejszej książki dzięki swoim pora­ dom, komentarzom i poświęconemu czasowi. Wydawnictwo Addison-Wesley stworzyło doskonały zespól redaktorów, do których należeli: Paul C. Clements, Robert L. Glass. Scott D. Meyers, Guy Steele. Dave Thomas oraz John Vlissides. Z ochotą przeczytali oni rękopis, który znajdował się w postaci znacznie mniej przystępnej niż książka, którą obecnie Czytelnik trzyma w rękach, i podzielili się z Autorem swoim doświad­ czeniem oraz wiedzą poprzez przemyślane, uważne i często bardzo odkrywcze recen­ zje. Ponadto Eliza Fragaki. Georgios Chrisoloras, Kleanthis Georgaris, Isidor Kouvelas oraz Lorenzo Vicisano byli czytelnikami fragmentów rękopisu i przedstawili wiele przy­ datnych komentarzy i sugestii. Autor miał również szczęście uzyskać porady odnośnie do zasad procesu produkcyjnego od Billa Cheswicka. Christine Hogan, Toma Limoncelliego oraz Antonisa Tsolomitisa. Poza tym. George Gousios zasugerował wykorzy­ stanie serwera Tomcat w formie materiału programistycznego open-source napisanego w'języku Java oraz wyjaśnił Autorowi szczegóły dotyczące jego działania, poradził wykorzystanie narzędzia ant, a także omówił kw'estie związane z wykorzystaniem for­ matu dokumentacji DocBook. Stephen Ma rozwiązał zagadkę funkcjonowania wskaź­ ników vnode na poziomie sterowników urządzeń systemu operacyjnego (patrz podroz­ dział 9.1.4). Spyros Oikonomopoulos przedstawił Autorowi zarys możliwości inżynierii wstecznej oferow'anych przez narzędzia do modelowania oparte na języku UML. Panagiotis Petropoulos zaktualizował odwołania zawarte w książce. Konstantina Vassilopoulou przekazała porady dotyczące aspektu czytelności listingów opatrzonych komen­ tarzami. Ioanna Grinia, Vasilis Karakoidas, Nikos Korfiatis, Vasiliki Tangalaki oraz George M. Zouganelis przedstawili swoje poglądy na temat układu książki.

14

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Elizabeth Ryan oraz pracownicy ITC cierpliwie projektowali i przeprojektowywali książkę do momentu, kiedy obie strony uznały, że jej forma jest odpowiednia. Redaktorzy Ross Venables i Mikc Hendrickson z wydawnictwa Addison-Wesley z godną uznania skutecznością kierowali procesem tworzenia książki. Latem 2001 roku, tydzień po pierwszym nawiązaniu kontaktu, Ross Venables przesłał już do przejrzenia propo­ zycję rękopisu. Praca w warunkach siedmiogodzinnej różnicy czasowej sprawiła, że odpowiedzi na pytania zadane jednego dnia Autor mógł znaleźć w wiadomościach poczty elektronicznej rano dnia następnego. Niesłychana skuteczność w zapewnieniu odpo­ wiednich recenzentów, udzielanie odpowiedzi na często naiwne pytania, zajęcie się kwestiami prawnymi związanymi z pisaniem książki oraz koordynowanie złożonego procesu produkcyjnego odegrały największą rolę w zapewnieniu powodzenia całego przedsięwzięcia. Elizabeth Ryan potrafiła również zsynchronizować działania zespołu produkcyjnego wydawnictwa Addison-Wesley. Chrysta Meadowbrooke pracowicie przeprowadziła korektę rękopisu książki, demonstrując godne podziwu zrozumienie technicznych aspektów jej treści. Pracownicy ITC byli odpowiedzialni za niełatwe zadanie składu książki. W końcu, Jennifer Lundberg cierpliwie wprowadziła Autora w tajniki rynku księgarskiego. Ogromna większość przykładów wykorzystanych w książce stanowi fragmenty istnieją­ cych projektów open-source. Użycie realnego kodu pozwoliło na zaprezentowanie rodzaju kodu, z jakim ma się zwykle do czynienia, nie zaś uproszczonych programów-zabawek. Dlatego też Autor pragnąłby podziękować wszystkim osobom udostępniającym swoje dzieła na zasadzie open-source za dzielenie się swoimi doświadczeniami ze społeczno­ ścią programistów. Nazwiska autorów fragmentów kodu. które zawarto w niniejszej książce, zamieszczono w dodatku B.

Rozdział 1.

Wprowadzenie Kod źródłowy to rozstrzygająca metoda opisu operacji wykonywanych przez program oraz przechowywania wiedzy w postaci wykonywalnej. Kod źródłowy można skompi­ lować do postaci wykonywalnej, można go przejrzeć w celu zrozumienia, co program wykonuje i jak działa, można go także zmodyfikować w celu dokonania zmian w jego działaniu. W przypadku większości szkoleń programistycznych i podręczników uwaga jest skupiona na sposobach pisania programów od podstaw. Jednakże 40 - 70% wy­ siłku związanego z opracowaniem systemu programistycznego wiąże się z działaniami podejmowanymi już po jego napisaniu. Wysiłki te są nieuchronnie związane z czyta­ niem, analizowaniem i modyfikowaniem oryginalnego kodu. Ponadto nieuniknione nagromadzenie starego kodu, przykładanie coraz większej wagi do technik wielokrot­ nego jego wykorzystania, wysokie koszta ludzkie związane z przemysłem programi­ stycznym oraz rosnące znaczenie projektów open-source i procesów programowania kooperacyjnego (w tym outsourcing, przeglądanie kodu i programowanie ekstremalne) sprawiają, że umiejętność czytania kodu nabiera fundamentalnego znaczenia dla współ­ czesnych inżynierów oprogramowania. Poza tym czytanie rzeczywistego, dobrze napi­ sanego kodu może dać programiście wgląd w metody projektowania struktury i two­ rzenia kodu skomplikowanych systemów — umiejętności tej nie da się zdobyć pisząc wyłącznie proste programy-zabawki. Programy powinny być pisane tak, aby dało się je czytać, ale bez względu na to, czy tak rzeczywiście jest, konieczność ich czytania nie podlega dyskusji. Chociaż czytanie kodu jest, cytując słowa Roberta Glassa. „czynno­ ścią niedocenianą, w niedostatecznym stopniu nauczaną” [GlaOO]1. nie musi tak być. Dzięki niniejszej książce Czytelnik pozna techniki czytania kodu pisanego przez innych na podstawie konkretnych przykładów wziętych z istotnych, rzeczywistych systemów open-source. Przyjmiemy liberalną definicję kodu i będziemy uwzględniać wszelkie, czytane przez maszynę, elementy projektu: kod źródłowy (wraz z komentarzami), doku­ mentację, programy wykonywalne, repozytoria kodu źródłowego, diagramy projektowe oraz skrypty konfiguracyjne. Zgłębiwszy dokładnie treść niniejszej książki. Czytelnik:

1 Powyższy symbol oznacza odwołanie do pozycji zawartej w znajdującej się na końcu książki bibliografii. Każdy tytuł posiada przypisany podobny kod — pierwsze litery nazwiska autora(ów) i dwie ostatnie cyfry roku wydania umieszczone w nawiasie kwadratowym. — przyp. tłum.

16

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

♦ będzie potrafił czytać i analizować skomplikowany kod; ♦ doceni wiele istotnych pojęć z zakresu tworzenia oprogramowania; ♦ pozna metody badania dużych fragmentów kodu; ♦ będzie potrafił czytać kod zapisany w wielu ważniejszych wysokoi niskopoziomowych językach programowania; ♦ zda sobie sprawę z poziomu złożoności rzeczywistych projektów programistycznych. Choć książka rozpoczyna się od przeglądu podstawowych struktur programistycznych, zakłada się, że Czytelnik zna któryś z języków: C, C++ lub Java i potrafi używać pro­ stych narzędzi w celu badania prezentowanych przykładów kodu. Ponadto bliższe pozna­ nie omawianych systemów i aplikacji, choć nie wymagane, może ułatwić zrozumienie prezentowanego materiału. W pozostałej części niniejszego rozdziału Czytelnik znajdzie omówienie różnych powo­ dów, dla których zachodzi konieczność czytania kodu wraz z opisami odpowiednich strategii czytania oraz krótką „instrukcją” poznawania kolejnych fragmentów książki. Przyjemnego czytania (kodu)!

1.1. Motywy i metody czytania kodu Niekiedy czytamy kod dlatego, że jesteśmy do lego zmuszeni — na przykład poprawiając, badając lub ulepszając istniejący kod. Kiedy indziej czyta się kod w celu poznania me­ chanizmów działania pewnych rozwiązań, podobnie jak inżynierowie badają strukturę urządzeń. Kod można również czytać w celu wyszukania materiału, który można by wykorzystać powtórnie lub (rzadko, ale Autor ma nadzieję, że po lekturze niniejszej książki nieco częściej) dla zwykłej przyjemności — tak, jak czyta się dzieła literackie. Czytanie kodu z któregoś z wyżej wymienionych powodów jest związane z określonym zestawem technik, które podkreślają różne aspekty posiadanych umiejętności".

1.1.1. Kod jako literatura Dick Gabriel twierdzi, że programista to jeden z nielicznych zawodów twórczych, w przypadku których piszący nie mogą czytać dzieł innych piszących [GGOO], Efektem praw' własności jest brak jakichkolwiek możliwości traktowania oprogramowania w sposób podobny do literatury. Sytuacja wygląda tak, jak gdyby wszyscy pisarze posiadali własne prywatne przedsiębiorstwa i na przykład tylko osoby z firmy Melville’a mogły czytać Moby-Dicka, zaś Słońce też wschodzi mogli czytać wyłącznie pracownicy firmy Hemingwaya. Czy można wyobrazić sobie rozw'ój literatury w takich 2





»

" Autor jest wdzięczny Dave’owi Thomasowi za zasugerowanie napisania niniejszego podrozdziału.

Rozdział ±. ♦ Wprowadzenie

17

warunkach? Nie powstałyby wówczas programy nauczania literatury ani techniki nauki pisania. A jednak oczekujemy, że ludzie będą się uczyć programowania właśnie w takich warunkach. Oprogramowanie open-source zmieniło tę sytuację: obecnie posiadamy dostęp do mi­ lionów wierszy kodu (o różnej jakości), który można czytać, oceniać, ulepszać i uczyć się na jego podstawie. W rzeczywistości wiele procesów społecznych, które przyczy­ niły się do sukcesu twierdzeń matematycznych jako nośników komunikacji naukowej, ma zastosowanie również w przypadku oprogramowania open-source. Większość pro­ gramów open-source: ♦ została udokumentowana, opublikowana i przeanalizowana w postaci kodu źródłowego; ♦ została omówiona, umiędzynarodowiona, uogólniona i udoskonalona; ♦ została wykorzystana do rozwiązania faktycznie występujących problemów, często w połączeniu z innymi programami. Warto wyrobić w sobie nawyk czytania kodu wysokiej jakości pisanego przez innych. Podobnie jak czytanie wartościowych dzieł literackich pozwala wzbogacić słownictwo, rozwija wyobraźnię i poszerza horyzonty myślowe, tak samo badanie struktury popraw­ nie zaprojektowanego systemu pozwala poznać nowe wzorce architektury, struktury danych, metody kodowania, algorytmy, konwencje używanych stylów i dokumentacji, programistyczne interfejsy aplikacji (ang. application programrning interface. API) czy wręcz nowy język programowania. Czytanie kodu wysokiej jakości często pozwala również na podniesienie jakości własnego kodu. W toku doświadczeń związanych z czytaniem kodu źródłowego każdy nieuchronnie spotka kiedyś taki kod, który w najlepszym razie można traktować jako przykład roz­ wiązań, jakich należy unikać. Zdolność szybkiego odróżniania kodu dobrego od złego ma duże znaczenie. Poznawanie pewnych kontrprzykładów w stosunku do poprawnych metod programowania może pomóc w rozwijaniu własnych umiejętności. K.od niskiej jakości można rozpoznać na podstawie następujących cech: ♦ niespójny styl zapisu kodu programu; ♦ bezzasadnie skomplikowany lub nieczytelny kod; ♦ występowanie oczywistych błędów lub przeoczeń logicznych; ♦ zbyt częste używanie nieprzenośnych konstrukcji; ♦ brak utrzymania kodu. Nie należy jednak oczekiwać, że na podstawie źle pisanego kodu można nauczyć się programować poprawnie. Jeżeli kod czyta się w sposób podobny do sposobu czytania dzieł literackich, jest to marnowanie czasu, szczególnie biorąc pod uwagę ilość dostęp­ nego obecnie kodu wysokiej jakości. Należy zadać sobie pytanie: czy kod. który czytam, rzeczywiście jest reprezentatywną próbką dokonań programistycznych w danej dziedzinie? Jedną z korzyści związanych z ruchem open-source jest to, że udane projekty programistyczne i pomysły inicjują

18

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

atmosferę zdrowej rywalizacji w zakresie ulepszania ich struktur lub oferowanych funkcji. Często mamy to szczęście, że możemy zapoznać się z drugą lub trzecią wersją danego projektu programistycznego. W większości przypadków (choć nie zawsze) późniejszy projekt stanowi znaczne udoskonalenie w porównaniu z wersjami wcześniejszymi. Przeprowadzenie w sieci WWW wyszukiwania z użyciem słów kluczowych dotyczą­ cych interesujących nas funkcji pozwala łatwo znaleźć konkurujące implementacje. Kod należy czytać wybiórczo i w konkretnym celu. Czy chcemy nauczyć się nowych wzorców, stylu kodowania, a może metod spełniania określonych wymagań? Jednak niekiedy warto również przeglądać kod w poszukiwaniu przypadkowych perełek. W ta­ kim przypadku należy być przygotowanym na konieczność dogłębnego zbadania intere­ sujących elementów, których się nie zna: funkcji języka (nawet jeśli zna się dobrze dany język — współczesne języki ewoluują), interfejsów AP1. algorytmów, struktur danych, architektury lub wzorców projektowych. Należy zwracać uwagę i uwzględniać określone niefunkcjonalne wymagania związane z kodem, które mogą spowodować powstanie pewnego stylu implementacji. Wymagania dotyczące przenośności, wydajności czasowej lub pamięciowej, czytelności lub nawet zaciemnienia mogą w rezultacie dać kod o bardzo swoistej charakterystyce. ♦ Istnieją programy, w których używa się sześcioliterowych identyfikatorów zewnętrznych w celu zachowania przenośności w zakresie konsolidatorów starszej generacji. ♦ Istnieją wydajne algorytmy, które posiadają implementacje o złożoności (pod względem liczby wierszy kodu źródłowego) większej o dwa rzędy wielkości w porównaniu z analogicznymi prostymi rozwiązaniami. ♦ Kod aplikacji osadzonych lub posiadających znaczne ograniczenia co do zajętości pamięci (wystarczy wziąć pod uwagę różne dystrybucje dyskietkowe systemów GNU/Linux lub FreeBSD) może zawierać dość skomplikowane rozwiązania zapewniające zaoszczędzenie kilku bajtów pamięci. ♦ Kod pisany w celu zademonstrowania funkcjonowania algorytmu może wykorzystywać identyfikatory, które mają niepraktycznie długie nazwy. ♦ W niektórych dziedzinach zastosowania, na przykład w przypadku schematów zabezpieczeń przed nielegalnym kopiowaniem, może być konieczne zapewnienie nieczytelności kodu w (często nadaremnym) dążeniu do zapobieżenia działaniom inżynierii wstecznej. Kiedy czyta się kod należący do którejś z wymienionych kategorii, należy pamiętać o określonych niefunkcjonalnych wymaganiach, sprawdzając, na ile autorowi udało się je spełnić. Niekiedy może się okazać, że czytany kod pochodzi z zupełnie nam nieznanego śro­ dowiska (języka programowania, systemu operacyjnego lub interfejsu API). Jednak posiadając ogólną wiedzę na temat programowania i odpowiednich pojęć z zakresu informatyki, w wielu przypadkach można wykorzystać kod źródłowy jako sposób na­ uczenia się podstaw określonego środowiska. Należy jednak rozpocząć od prostych przykładów — nie warto od razu porywać się na analizę złożonego systemu. Należy

Rozdział 1. ♦ Wprowadzenie

19

również kompilować, konsolidować i uruchamiać analizowane programy. Zapewnia to zarówno natychmiastową informację zwrotną dotyczącą sposobu działania kodu, jak również poczucie osiągnięcia konkretnych celów. Kolejnym etapem jest dokonanie zmian w kodzie w celu sprawdzenia jego zrozumienia. Tu również należy zaczynać od małych zmian i stopniowo powiększać ich zakres. Aktywne obcowanie z prawdziwym kodem może pomóc w bardzo szybkim opanowaniu podstaw środowiska. Kiedy uzna się, że zostało to osiągnięte, warto rozważyć możliwość bliższego poznania środowiska w bardziej uporządkowany sposób — czytając książki, dokumentacje, instrukcje lub uczestnicząc w szkoleniach. Obie metody nauki wzajemnie się uzupełniają. Kolejny aspekt czytania istniejącego kodu traktowanego jako literatura to chęć jego ulepszenia. W przeciwieństwie do innych dzieł literackich, kod programów to żywa struktura podlegająca ciągłym ulepszeniom. Jeżeli kod stanowi wartość dla czytające­ go lub jego środowiska, warto pomyśleć o sposobach jego ulepszenia. Może to wiązać się z wykorzystaniem lepszego projektu lub algorytmu, utworzeniem dokumentacji pewnych fragmentów kodu lub dodaniem określonych funkcji. Kod open-source jest często słabo udokumentowany — być może warto rozważyć możliwość zawarcia zdoby­ tej wiedzy w poprawionej dokumentacji. Pracując z istniejącym kodem należy konsulto­ wać swoje wysiłki z jego autorami lub osobami odpowiedzialnymi za jego utrzymanie w celu uniknięcia duplikacji działań lub niezdrowych niedomówień. Jeżeli wprowadzane zmiany są istotne, należy rozważyć wykorzystanie systemu kontroli wersji (ang. con­ current versions system, CVS). Możliwe będzie wówczas bezpośrednie zatwierdzanie kodu w bazie źródeł projektu. Warto potraktować korzyści płynące z oprogramowania open-source jako swego rodzaju pożyczkę. Jest wówczas rzeczą oczywistą, że należy poszukać sposobów jej spłacenia poprzez przekazanie środowisku open-source czegoś od siebie.

1.1.2. Kod jako model Niejednokrotnie zastanawiamy się, w jaki sposób jest realizowana określona funkcja. W przypadku pewnych klas zastosowań odpowiedzi na stawiane pytania można zna­ leźć w standardowych podręcznikach lub specjalistycznych publikacjach i artykułach naukowych. Jednak w wielu przypadkach, jeżeli chcemy się dowiedzieć „w jaki sposób oni to zrobili”, nie ma lepszego sposobu niż przyjrzenie się kodowi. Czytanie kodu to prawdopodobnie również najbardziej godny zaufania sposób tworzenia oprogramowa­ nia kompatybilnego z określoną implementacją. Kluczowe znaczenie dla procesu wykorzystywania kodu jako modelu ma elastyczność. Trzeba być przygotowanym na wykorzystanie wielu różnych strategii i podejść w celu zrozumienia, w jaki sposób działa kod. Należy rozpocząć od zapoznania się z dostęp­ ną dokumentacją (patrz rozdział 8.). Formalny dokument projektu programistycznego to ideał, jednak nawet dokumentacja użytkownika może okazać się pomocna. Należy popracować w systemie w celu zapoznania się z jego interfejsami zewnętrznymi. Trze­ ba również określić, czego dokładnie się szuka: wywołania funkcji systemu, algorytmu, sekwencji kodu. architektury? Następnie należy opracować strategię, która pozwoli na odkrycie poszukiwanego elementu. Różne strategie wyszukiwania są skuteczne w przy­ padku różnych zastosowań. Może się okazać, że konieczne jest zbadanie sekwencji

20

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

wykonywania instrukcji, uruchomienie programu i umieszczenie punktów przerwań w najważniejszych punktach albo tekstowe przeszukanie kodu w celu znalezienia okre­ ślonego fragmentu lub elementu danych. Narzędzia (patrz rozdział 10.) mogą tu znacz­ nie pomóc, ale nie należy pozwolić, aby któreś z nich stało się jedynym używanym. Jeżeli dana strategia nie daje szybko oczekiwanych wyników, należy ją zarzucić i spró­ bować czegoś innego. Trzeba pamiętać, że poszukiwany kod gdzieś się znajduje — trzeba jedynie go zlokalizować. Kiedy uda się już zlokalizować poszukiwany kod. należy go przeanalizować, ignoru­ jąc elementy nie mające związku z bieżącym problemem. Jest to umiejętność, której trzeba się nauczyć. Wiele ćwiczeń zawartych w niniejszej książce dotyczy właśnie tego zadania. Jeżeli zrozumienie kodu w jego oryginalnym kontekście jest trudne, należy skopiować go do pliku tymczasowego i usunąć z niego wszelkie fragmenty nie mają­ ce wdanej sytuacji znaczenia. Formalna nazwa, jaką nosi ten proces, to wycinkowanie (ang. slicing) — patrz podrozdział 9.1.6 — jednak jej zasady można zrozumieć spraw­ dzając. w jaki sposób wykorzystano ją nieformalnie w zawartych w niniejszej książce przykładach kodów opatrzonych komentarzami.

1.1.3. Utrzymanie kodu W jeszcze innych przypadkach kod, zamiast funkcjonować jako model, może raczej wymagać wprowadzenia poprawek. Jeżeli stwierdzi się występowanie błędu w dużym systemie, należy wykorzystać pewne strategie i taktyki w celu czytania kodu na stop­ niowo coraz wyższym poziomie szczegółowości do momentu aż uda się zlokalizować problem. Kluczowe znaczenie w tym przypadku ma wykorzystanie narzędzi. W celu zlokalizowania błędu należy korzystać z debuggera. ostrzeżeń kompilatora lub danych wyjściowych kodu symbolicznego, programu śledzącego wywołania w systemie, ofe­ rowanych przez bazę danych mechanizmów rejestrowania operacji języka SQL. narzę­ dzi przechwytujących pakiety w sieci czy programów' przechwytywania komunikatów systemów Windows (w rozdziale 10. zawarto więcej informacji na temat pomocy, jaką mogą zaoferować narzędzia w procesie czytania kodu). Należy przeanalizować kod od objawów danego problemu po jego źródło. Nie należy badać części nie mających bezpośredniego związku z bieżącym problemem. Program należy kompilować z włą­ czoną opcją debuggowania i wykorzystywać oferowaną przez debugger funkcję śledze­ nia z użyciem stosu (ang. stack trace), krokow'e wykonywanie programu oraz pułapki dla danych i kodu w celu zawężenia obszaru poszukiwań. Jeżeli wykorzystanie debuggera nastręcza problemów (debugowanie programów, które działają w tle. takich jak demony lub usługi systemu Windows, kod C++ oparty na szablonach, serwlety i kod wielowątkowy, jest niekiedy niezmiernie trudne), należy wziąć pod uwagę umieszczenie instrukcji drukowania komunikatów' w najważniejszych miejscach programu. Badając kod języka Java, należy rozw'ażyć użycie narzędzia AspectJ w celu zapewnienia sobie możliwnści wstawiania do kodu programu elementów, które będą wykonywane tylko w przypadku wystąpienia określonych warunków. Jeżeli pro­ blem ma związek z interfejsami systemu operacyjnego, mechanizm śledzenia wywołań funkcji systemowych często pozwala dość ściśle zlokalizować źródło problemu.

Rozdział 1. ♦ Wprowadzenie

21

1.1.4. Rozwój W większości sytuacji (według niektórych źródeł chodzi o ponad 80% przypadków) kod czyta się nie w celu poprawienia błędu, lecz dodania nowej funkcji, zmodyfikowania istniejącej, dostosowania jej do nowego środowiska i wymagań lub refaktoryzacji w celu ulepszenia jej cech niefunkcjonalnych. Podstawowe znaczenie w tego rodzaju przypad­ kach ma zachowanie selektywności w zakresie kodu wybieranego do badania. Zazwy­ czaj konieczne jest poznanie bardzo niewielkiego fragmentu całej implementacji syste­ mu. W praktyce można zmodyfikować system liczący milion wierszy kodu (na przykład kod typowego jądra systemu lub systemu opartego na oknach) poprzez wybiórcze po­ znanie i zmodyfikowanie jednego lub dwóch plików. Entuzjazm odczuwany w przypad­ ku sukcesu takiej operacji to coś, czego warto życzyć każdemu programiście. Poniżej pokrótce opisano strategię selektywnej pracy z dużym systemem: ♦ należy zlokalizować fragmenty kodu interesujące w danym kontekście: ♦ należy przeanalizować kolejne partie kodu osobno; ♦ należy określić związek fragmentu kodu z jego pozostałą częścią. W przypadku dodawania nowych możliwości do systemu, pierwszym zadaniem jest znalezienie implementacji podobnej funkcji, która zostanie wykorzystana jako szablon dla nowo tworzonej. Podobnie w przypadku modyfikowania istniejącej funkcji, naj­ pierw należy zlokalizować adekwatny kod. Aby przejść od specyfikacji funkcjonalności do implementacji kodu, należy prześledzić komunikaty lub wyszukać kod za pomocą słów kluczowych. Przykładowo, aby zlokalizować kod uwierzytelniania użytkownika w programie ftp . można przeszukać kod w celu znalezienia ciągu znaków Password3: 1f (pass — NULL) pass - getpasst"Password:”); n - commandCPASS is", pass).

Po zlokalizowaniu funkcji, należy zbadać jej implementację (analizując wszystkie czę­ ści kodu, które można uznać za istotne), opracować nową funkcję i zmianę oraz zloka­ lizować fragmenty, na które będzie ona miała wpływ, czyli te partie kodu, które będą wykorzystywać nowo tworzony fragment. Zwykle są to jedyne fragmenty kodu, które trzeba dogłębnie poznać. Przystosowywanie kodu do nowych środowisk jest zadaniem wymagającym zastoso­ wania innej strategii postępowania. Istnieją przypadki, gdy oba środowiska oferują podobne możliwości — może na przykład chodzić o przenoszenie kodu z systemu Sun Solaris na GNU/Linux lub z Uniksa do systemu Microsoft Windows. W takich przy­ padkach najwartościowszym narzędziem może okazać się kompilator. Od samego po­ czątku należy przyjąć założenie, że zakończyło się zadanie i spróbować skompilować system. Następnie należy metodycznie modyfikować kod, biorąc pod uwagę błędy kom­ pilacji i konsolidacji, do momentu aż osiągnie się poprawną kompilację i zweryfikuje funkcjonalność systemu. Okazuje się, że takie podejście znacznie zmniejsza ilość kodu, jaki należy przeczytać. Podobną strategię postępowania można przyjąć w przypadku wprowadzania modyfikacji do interfejsu funkcji, klasy, szablonu lub struktury danych. W wielu przypadkach zamiast ręcznie lokalizować wpływ wprowadzonych zmian, można 3 netbsdsrc/usr.binJftp/util.c. 265 - 267.

22

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

posłużyć się komunikatami o błędach lub ostrzeżeniami kompilatora w celu znalezie­ nia fragmentów powodujących problemy. Poprawki wprowadzone w tych miejscach kodu często powodują powstanie nowych błędów — w toku tego procesu kompilator pokazuje miejsca w kodzie, na które ma wpływ nowo zapisany kod. Kiedy nowe środowisko różni się diametralnie od poprzedniego (na przykład lak jak w przypadku przenoszenia narzędzia sterowanego z poziomu wiersza poleceń do gra­ ficznego środowiska opartego na oknach), zachodzi potrzeba wykorzystania innego podejścia. W takiej sytuacji jedyną nadzieją na ograniczenie wysiłków związanych z czytaniem kodu jest skupienie się na elementach, w których implementacja starego kodu różni się od nowego. We wspomnianym przykładzie oznaczałoby to skupienie się na kodzie interakcji z użytkownikiem i zupełne zignorowanie wszelkich aspektów algorytmicznych systemu będącego obiektem zainteresowania. Całkowicie odmienna klasa zmian wprowadzanych w kodzie dotyczy refaktoryzacji (ang. refactoring). Takie zmiany nabierają coraz większego znaczenia w miarę jak pewne rodzaje działań projektowych zaczynają wykorzystywać metodologię programowania ekstremalnego i programowania zwinnego. Refaktoryzacja jest związana z wprowadze­ niem do systemu zmiany, która nie zmienia jego statycznego zewnętrznego zachowa­ nia. ale poprawia pewne jego cechy niefunkcjonalne, takie jak prostota, elastyczność, zrozumiałość lub wydajność. Refaktoryzacja ma wiele wspólnego z chirurgią plastycz­ ną. W przypadku refaktoryzacji rozpoczynamy od działającego systemu i chcemy za­ pewnić, aby po zakończeniu działań również był on sprawny. Zestaw adekwatnych przypadków testowych pomaga w spełnieniu tego wymogu, więc należy zacząć właśnie od ich zapisania. Jeden z rodzajów refaktoryzacji dotyczy poprawiania znanego proble­ mu. Należy w tym przypadku poznać i zrozumieć stary kod (o czym traktuje niniejsza książka), opracować nową implementację, zbadać jej wpływ na kod powiązany z nowo utworzonym kodem (w wielu przypadkach nowy kod stanowi podmianę starego) oraz wprowadzić zmianę. Inny rodzaj refaktoryzacji jest związany ze spędzeniem pewnego okresu czasu z danym systemem programistycznym, aktywnie wyszukując kod. który można by usprawnić. Jest to jeden z nielicznych przypadków, w których należy znać ogólny obraz projektu i architektury systemu. Refaktoryzacja na dużą skalę zwykle przynosi więcej korzyści niż na niewielką skalę. W rozdziale 6. omówiono sposoby radzenia sobie z dużymi systemami, natomiast w rozdziale 9. opisano sposób przejścia od kodu do architektury systemu. Czytając kod w celu wyszukania możliwości zastosowania refaktoryzacji, moż­ na maksymalizować osiągane korzyści rozpoczynając od architektury systemu i scho­ dząc w dół, prowadząc poszukiwania na coraz wyższym poziomie szczegółowości.

1.1.5. Ponowne wykorzystanie Niekiedy również kod czyta się w celu znalezienia elementów możliwych do ponow­ nego wykorzystania. Podstawowe znaczenie w tym przypadku ma ograniczenie swo­ ich oczekiwań. Możliwość ponownego wykorzystania kodu jest kuszącą, ale złudną perspektywą. Należy ograniczyć swoje oczekiwania, by nie rozczarować się. Pisanie kodu nadającego się do wielokrotnego wykorzystania jest niezmiernie trudne. Przez całe lata stosunkowo niewielka część napisanego oprogramowania z powodzeniem przeszła próbę czasu i została wykorzystana w wielu różnych sytuacjach. Konstrukcje

Rozdział 1. ♦ Wprowadzenie

23

programistyczne stają się zwykle kandydatami do ponownego wykorzystania dopiero po wprowadzeniu odpowiednich rozszerzeń i ich iteracyjnym zaadaptowaniu do pracy w dwóch lub trzech różnych systemach. Nieczęsto się tak dzieje w przypadku oprogra­ mowania tworzonego z potrzeby chwili. W rzeczywistości, zgodnie z modelem szaco­ wania kosztów programowania COCOMO II [BCH 95], tworzenie oprogramowania nadającego się do wielokrotnego wykorzystania może zwiększyć nawet o 50% wyma­ gany wysiłek programistyczny. Szukając kodu możliwego do ponownego wykorzystania w przypadku określonego problemu, najpierw należy wyizolować kod. który rozwiązuje ten problem. Przeszuka­ nie kodu systemu po słowach kluczowych zwykle pozwala dotrzeć do implementacji. Jeżeli kod, który chce się ponownie wykorzystać, jest skomplikowany, trudny do zro­ zumienia i wyizolowania, należy raczej przyjrzeć się pakietom zapewniającym wyższy poziom abstrakcji lub poszukać innego kodu. Przykładowo, zamiast usilnie starać się zrozumieć skomplikowane relacje istniejące między fragmentem kodu a innymi ele­ mentami, warto rozważyć wykorzystanie całej biblioteki, komponentu, procesu lub nawet całego systemu, którego część stanowi dany kod. Innego rodzaju działania związane z ponownym wykorzystaniem kodu polegają na aktywnym badaniu kodu w poszukiwaniu wartościowych, niewielkich fragmentów kodu możliwego do wielokrotnego wykorzystania. W tym przypadku najlepszym roz­ wiązaniem jest poszukanie kodu. który już jest wielokrotnie wykorzystywany — praw­ dopodobnie w samym systemie, który podlega badaniu. Pozytywne sygnały wskazujące na występowanie takiego kodu to na przykład używanie odpowiednich metod zastoso­ wania pakietów (patrz podrozdział 9.3) lub mechanizmu konfiguracyjnego.

1.1.6. Inspekcje

A

W końcu, w niektórych sytuacjach zadanie czytania kodu może stanowić integralną część wykonywanej pracy. Wiele metodologii tworzenia oprogramowania wykorzystuje przeglądy techniczne, takie jak kontrole ogólne, inspekcje, przeglądy cykliczne oraz inne rodzaje dokonywania technicznych ocen jako integralnej części procesu tworzenia. Ponadto w przypadku wykorzystywania rozwiązania polegającego na programowaniu w parach i jednocześnie stosowaniu metodologii programowania ekstremalnego często okazuje się, że jedna osoba czyta kod pisany przez drugą. Czytanie kodu w takich wa­ runkach wymaga innego poziomu zrozumienia, oceny i uwagi — należy być dokładnym. Należy badać kod w celu odkrycia błędów funkcjonalnych i logicznych. Różne elementy oznaczane na marginesie odpowiednim symbolem jako groźne (patrz symbol obok) nie powinny umykać uwadze. Poza tym należy być gotowym do omówienia elementów, które się przeoczy. Trzeba ponadto sprawdzać, czy kod spełnia wszystkie stawiane wymagania. Kwestie niefunkcjonalne dotyczące kodu powinny być traktowane z jednakową uwagą. Czy kod odpowiada standardom projektowym i wskazówkom dotyczącym stosowanego stylu obowiązującym w przedsiębiorstwie? Czy istnieje możliwość przeprowadzenia refaktoryzacji? Czy danego fragmentu nie można by zapisać w sposób bardziej czytelny lub wydajny? Czy pewne elementy nie mogłyby wykorzystywać istniejących bibliotek lub komponentów? Analizując system programistyczny należy pamiętać, że składa się

24

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

on z większej liczby elementów niż instrukcja wykonawcza. Trzeba zbadać strukturę plików i katalogów, proces kompilacji, konsolidacji i konfigurowania, interfejs użyt­ kownika oraz dokumentację systemową. Inspekcje oprogramowania i działania pokrewne są związane z interakcją z użytkow­ nikiem. Przeglądy oprogramowania powinny być traktowane jako szansa nauczenia czegoś siebie i innych, udzielenia pomocy komuś lub otrzymania jej dla siebie.

1.2. Jak czytać tę książkę? W niniejszej książce zostały zaprezentowane ważne techniki czytania kodu. zarysowano tu także najczęściej spotykane pojęcia programistyczne w formie, w jakiej występują w realnym świecie. Jej celem jest poprawienie zdolności Czytelnika w zakresie czytania kodu. Choć w kolejnych rozdziałach zawarto omówienie wielu istotnych pojęć z dzie­ dziny informatyki i technik obliczeniowych — takich jak struktury danych i struktury sterujące, standardy kodowania oraz architektura oprogramowania — ich opis jest, z konieczności, jedynie pobieżny, gdyż celem książki jest nakłonienie Czytelnika do samodzielnego badania sposobów ich wykorzystania w kontekście kodu produkcyjne­ go, a nie wprowadzanie pojęć jako takich. Materiał został pogrupowany w taki sposób, że pozwala na stopniowe poznawanie coraz bardziej złożonych elementów. Jednakże jest to podręcznik, a nie powieść detektywistyczna, więc nie ma żadnych przeciwwska­ zań, aby zapoznać się z jej treścią w dowolnej kolejności.

1.2.1. Konwencje typograficzne Wszystkie listingi oraz odwołania do elementów programów występujące w tekście (na przykład nazwy funkcji, słowa kluczowe lub operatory) są zapisywane czcionką 0 s ta łe j szerokości. Niektóre prezentowane przykłady odnoszą się do sekwencji po­ leceń wykonywanych z poziomu powłoki systemu Unix lub Windows. W takich przy­ padkach zostaje zawarty znak zachęty powłoki $, oznaczający polecenia powłoki sys­ temu Unix, lub sekwencja C:>, oznaczająca znak zachęty konsoli systemu Windows. Polecenia powłoki systemu Unix mogą rozciągać się na wiele wierszy — jako symbolu kontynuacji wiersza używamy znaku >: $ grep -1 malloc *.c |

> wc -1 8 C >grep -1 malloc *.c 1 wc -1

8

Znaki zachęty oraz symbol kontynuacji wiersza są wyświetlane jedynie w celu odróż­ nienia tekstu wpisywanego przez użytkownika od tekstu wyświetlanego przez system: należy wpisywać jedynie polecenia znajdujące się po znaku zachęty.

A Niekiedy omawiane są niebezpieczne nawyki programistyczne lub często spotykane pułapki. Są one oznaczane odpowiednim symbolem umieszczanym na marginesie. W przypadku przeprowadzania przeglądu kodu lub po prostu czytając kod w poszu-

Rozdział 1. ♦ Wprowadzenie

25

P I kiwaniu błędu, należy starać się nie przeoczyć tego rodzaju fragmentów. Tekst ozna­ czony na marginesie literą „i” oznacza często występujące idiomy programistyczne. Czytając tekst człowiek wykazuje tendencję do rozpoznawania całych wyrazów, a nie poszczególnych liter. Podobnie rozpoznawanie takich idiomów w kodzie pozwala na jego szybsze i wydajniejsze czytanie oraz daje możliwość zrozumienia programów na wyższym poziomie. Przykłady kodu używane w niniejszej książce pochodzą z programów, które faktycznie są używane na świecie. Opisywany w danym momencie program (na przykład przedstawio­ ny na listingu 1.1) jest identyfikowany przez odpowiedni przypis dolny-1, który zawiera precyzyjne określenie lokalizacji programu w drzewie katalogów płyty CD-ROM dołą­ czonej do książki, jak również numery wierszy, z których pochodzi dany fragment. Kiedy obok siebie znajdują się fragmenty pochodzące z różnych plików źródłowych (jak ma to na przykład miejsce w przypadku listingu 5.16 ze strony 169), przypis okre­ śla katalog, w którym znajdują się dane pliki5. Listing 1.1. Przykład listingu opatrzonego komentarzami ma1n(argc. argv)

+

— Komentarz prosty

[ . .. ]

#

Pominięty kod

{

1f (argc > 1) • --- [ t] Komemarz związany t opisem \\yslępu/qcym h tekście fort:;) (vold)puts(argv[l]): else for 0) { i f (fre o p e n (a rg v [0 ]. " r * . s td ln ) — NULL) ( p e rro r(a rg v[O J): e x 1 t( l) :

}

a rg e --. a r g v « :

column - 0: w hile ( (c - g e tc h a rO ) ! - E O f f j

'[7] Odczytuj znaki aż do napotkania znaku EOF

switch (c) { case ' V t' : • -------------------------

'Znak tabulatora

1f (nstops — 0) { do ( putchar(' ‘): column«; ) while (column & 07): continue: • ---------------------¡8] Przetwórz kolejm znak

)

if (nstops — 1) { do { putcharC '): column«: ) while (((column - U I tabstops[Q!) !- (tabstops[0] - 1)). continue: • ---------------------[8]

for (n » 0: n < nstops: n«) if (tabstops[n] > column) break: if (n — nstops) { putcharC '): column«: continue: • ------------------------ ¡8]

)

while (column < tabstopsfn]) { putcharC column«:

} continue: case ’ \ b ‘ : ®

• ----------------------------- [8] znak cofnięcia

if (column) column--; putcharC\b'). continue. d e fa u lt:®

• ----------------------------- ¡8] Wszystkie inne znaki

putchar(c); column«. continue: • ----------------------------- [8] case ' \ n ' : • -------------------------------------Znak nowego wiersza

putchar(c); column « 0.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

41

contlnue: • --------------------------- ¡H] }• Koniec htoku swltch } • -----------------------------------------------------------Koniec bloku wtli 1e

} while (argc > 0), • -------------------- Koniec bloku do exit(0):

W przypadku badania niebanalnego programu, przydatną metodą jest zidentyfikowa­ nie w pierwszej kolejności jego głównych części. W naszym przypadku są to zmienne globalne (listing 2.2:1) oraz funkcje main (listing 2.3), getstops (patrz listing 2.4:1) i usage (patrz listing 2.4:8). Zmienna typu całkowitego nstops oraz tablica liczb całkowitych tabstops są deklaro­ wane jako zmienne globalne (ang. global variables) poza zakresem bloków funkcyjnych. Dlatego też są widoczne z poziomu wszystkich funkcji występujących w badanym pliku. fi~| Trzy zapowiedzi funkcji występujące powyżej (listing 2.2:2) deklarują funkcje, które występują w dalszej części pliku. Ze względu na fakt, że niektóre z tych funkcji są uży­ wane zanim zostaną zdefiniowane, w programach C i C++ deklaracje zapowiadające umożliwiają kompilatorowi weryfikację argumentów przekazanych do funkcji i ich wartości zwracanych oraz wygenerowanie odpowiedniego poprawnego kodu. Kiedy nie zostaną określone deklaracje zapowiadające, kompilator języka C określa typ wartości zwracanej przez funkcję oraz typy jej argumentów w momencie jej pierwszego użycia (kompilator języka C++ traktuje takie przypadki jako błędne). Jeżeli występująca w dalszej części programu definicja funkcji nie odpowiada przyjętym założeniom, A kompilator generuje ostrzeżenie lub komunikat o błędzie. Jednak jeżeli błędna deklara­ cja dotyczy funkcji zdefiniowanej w innym pliku, program może się kompilować bez żadnych problemów i ulegać awarii dopiero w czasie wykonania.

A

Warto zwrócić uwagę na fakt, że dwie funkcje są deklarowane jako static, w przeci­ wieństwie do zmiennych. Oznacza to, że funkcje te są widoczne jedynie w ramach pliku, natomiast zmienne mogą być potencjalnie widoczne z poziomu wszystkich plików składających się na program. Program expand składa się tylko z jednego pliku, więc w tym przypadku rozróżnienie to nie ma znaczenia. Większość konsolidatorów, które łączą skompilowane pliki C, jest dość prymitywna. Zmienne, które są widoczne dla wszystkich plików programu (to znaczy, nie są zadeklarowane jako static) mogą w dziwny sposób oddziaływać na zmienne posiadające te same nazwy zdefiniowane w innych plikach. Stąd dobrą praktyką podczas badania kodu jest zapewnienie, aby wszystkie zmienne potrzebne tylko w jednym pliku były deklarowane jako statyczne. Poniżej przyjrzymy się funkcjom składającym się na program expand. W celu zrozu­ mienia, co dana funkcja (lub metoda) robi. można wykorzystać jedną z następujących strategii działania. ♦ zgadywanie w oparciu o nazwę funkcji; ♦ przeczytanie komentarza umieszczonego na początku funkcji; ♦ zbadanie sposobów użycia funkcji;

42

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

♦ odczytanie kodu treści funkcji; ♦ zapoznanie się z zewnętrzną dokumentacją programu. W naszym przypadku możemy bezpiecznie odgadnąć, że funkcja usage powoduje wy­ świetlenie informacji o zasadach używania programu, a następnie kończy jego działa­ li] nie. Wiele programów uruchamianych z poziomu wiersza poleceń posiada funkcję o tej samej nazwie i funkcjonalności. Badając duży fragment kodu. użytkownik stopniowo zapoznaje się z nazwami i konwencjami nazewnictwa zmiennych i funkcji. To pomaga w poprawnym odgadywaniu, jakie działania wykonują. Jednak zawsze należy być przy­ gotowanym na zweryfikowanie swoich początkowych przypuszczeń po zapoznaniu się z nowymi faktami, co jest nieuniknione w przypadku czytania kodu. Ponadto, w przy­ padku modyfikowania kodu w oparciu o przypuszczenia, należy zaplanować proces, który pozwoli na zweryfikowanie początkowych hipotez. Może on być związany z wy­ konaniem sprawdzeń za pomocą kompilatora, wprowadzeniem asercji lub wykonaniem odpowiednich testów. Działanie funkcji getstops jest nieco trudniejsze do zrozumienia. Nie zawarto żadnego komentarza, kod treści funkcji nie jest banalny, a sama nazwa funkcji może być róż­ norodnie interpretowana. Zauważenie, że jest ona używana w jednej części programu (listing 2.3:3) może nam jednak pomóc. Fragment programu, w którym jest używana funkcja getstops, służy do przetwarzania opcji programu (listing 2.3:2). Możemy więc bezpiecznie (i w tym przypadku poprawnie) założyć, że funkcja getstops przetwarza opcję specyfikacji znaków tabulacji. Taka forma stopniowego poznawania programu jest często stosowana w czasie czytania kodu; zrozumienie jednej części kodu może pozwolić poznać inne fragmenty. W oparciu o tę formę stopniowego poznawania pro­ gramu można opracować strategię radzenia sobie z trudnym kodem, podobną do tej, której używa się w celu ułożenia układanki: należy zacząć od prostych części. Ćwiczenie 2.7. Zbadaj widoczność funkcji i zmiennych w programach w swoim środowi­ sku. Czy można je ulepszyć (uczynić bardziej konserwatywnymi )? Ćwiczenie 2.8. Wybierz pewne funkcje lub metody z płyty CD-ROM dołączonej do książki lub z własnego środowiska i określ pełnione przez nie role. wykorzystując omówione strategie. Postaraj się zminimalizować czas poświęcony każdej funkcji lub metodzie. Uporządkuj strategie według ich skuteczności.

2.3. Pętle while, instrukcje warunkowe i bloki Możemy teraz zbadać, w jaki sposób przetwarzane są opcje. Chociaż program expand akceptuje tylko jedną opcję, wykorzystuje on funkcję biblioteczną systemu Unix getopt w celu przetworzenia opcji. Na rysunku 2.1 przedstawiono skróconą wersję dokumen­ tacji systemu Unix poświęconej funkcji getopt. Większość środowisk programistycz­ nych zapewnia dostęp do dokumentacji dotyczącej funkcji bibliotecznych, klas i metod.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

43

Rysunek 2.1.

Strona dokumentacji poświęcona fimkcji getopt

G E rrO K ri 3 >

U N IX Program m er \ M anual

G E T O IT l 3 )

NAM E g e to p t - g e t option c h ara cter from c o m m an d lin e a rgum ent list

SYNOPSIS • in c lu d e < u n lst< lh > e x te rn e x te rn e x te rn e x te rn e x te rn

c h a r « o p to rg . Int o p tin d ; Int o p t o p I; Int o p t e r r Int o p tre s e t.

int g e to p t i n t a r g c . c h a r -c o n s t - a r g v . c o n s t c h a r - o p ts tr in g »

DESCRIPTION T he g e to p t ( ) function in crem entally p a n e s a co m m a n d line argum ent list argv an d returns the ncxi k now n o p tion character. A n op tio n c h aracter is know n if it has been speci e d in the string o f accepted option characters, o p ts tn n q T he o p tion stn n g o p ts trin g m ay co n tain the follow ing elem ents: individual characters, and characters follow ed by a c o lo n to indicate a n option argum ent is to follow For exam ple, a n option stn n g V re c o g ­ n iz es a n op tio n “- x " and an op tio n strin g V rec o gnizes an option and argum ent *‘- x a rg u m e n t '* It do es not m atter to g e to p t i ) if a follow ing argum ent lias leading w hite space O n return from g e to p t 0 . optarg p o in ts to a n o p tion argum ent, if it 1» anticipated, and the vanable o pt m i co n tain s the index to th e next a r g v argum ent fo r a subsequent call to g e to p t 1 > T he v s n a b k o p to p t saves the last know n o p tio n c haracter returned by g e to p t (). T h e variable o p trrr and o p n n d arc both initialized to I T he o p tin d v ariable m ay be sci 10 another value be fore a set o f c a lls to g e to p t t 1 in o rd e r to sk ip o v e r m ote o r less argv entries The g e to p t ( ) function re turns -1 w hen the argum ent list is exhausted, o r a n o n -n x o g m rc d option is e n ­ countered. T he in terpretation o f o p tio n s m th e argum ent list m ay he cancelled by th e option - ' (double dash) w hich c auses g e to p t < ) to signal the en d o f argum ent processing and returns - I W hen all option* have b ee n p roce sse d (i.e.. u p to the ml non-option a r gum eni). g e to p t l ) returns - 1

DIAGNOSTICS If the g e t o p t ( ) function enco u n ters a ch aracter n o t found in the su in g o p ts trin g o r detect* a missing op tion argum ent it w rite* an e rro r m essage to \td e rr and returns ' ? ' Selling o p t e rr to a zero w ill disable these c m r m essag e s If o p tn n n g has a leading '. * then a m issing option argum ent cause* a ' ’ to be re­ lu m ed in a ddition to s u p pressing a ny e rro r m essages. O p tio n argum ents arc allow ed to begin w ith possible.

this is rea sonable but reduces th e am ouni o f c rm r checking

H IS T O R Y The g e to p t < ) function a p p e aled 4

BSD

BUGS The g e t o p t ( ) function w as o n ce s p e d e d to return EOF instead o f - I . Thi* was changed b> POS1X 1003.292 to d e co u p le g e t o p t ! ) fm m < itd io J h > .

4 .3 B erkeley D istribution

A pril 19. 1994

I

W systemach uniksowych można skorzystać z polecenia man, zaś w systemach Win­ dows z Microsoft Developer Network Library (MSDN)14. Z kolei środowisko Java API posiada dokumentację w formacie HTML jako część pakietu Sun JDK. Warto nabrać nawyku czytania dokumentacji elementów bibliotecznych, jakie się spotyka — pozwoli to podnieść zarówno umiejętności czytania, jak i pisania kodu. W oparciu o wiedzę na temat działania funkcji getopt możemy teraz zbadać odpowied­ ni kod (listing 2.3:2). Ciąg znaków opcji przekazywany do funkcji getopt dopuszcza użycie pojedynczej opcji -t, po której powinien wystąpić argument. Funkcja getopt jest używana jako wyrażenie warunkowe w pętli while. Instrukcja while powoduje powtarzanie swojej treści tak długo, jak warunek określony w nawiasach ma wartość prawdy (w przypadku języków C i C++ oznacza to, że jego wartość jest różna od 0). W naszym przypadku warunek pętli while wywołuje funkcję getopt, przypisuje otrzy­ many wynik do zmiennej c i porównuje ją z wartością -1, która oznacza, że wszystkie 14http://msdn.microsoft. com.

44

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

opcje zostały przetworzone. W celu wykonania tych operacji w ramach jednego wyra­ żenia w kodzie wykorzystano fakt, że w rodzinie języków wywodzących się z C przypi­ sanie jest wykonywane poprzez operator (=), to znaczy, wyrażenia przypisania posia­ dają wartość. Wartością wyrażenia przypisania jest wartość przechowywana w lewym operandzie (w naszym przypadku jest to zmienna c) po zakończeniu operacji przypisa[T] nia. Wiele programów wywołuje funkcję, przypisuje zwracaną przez nią wartość do zmiennej i porównuje otrzymany wynik z pewną wartością specjalną w ramach poje­ dynczego wyrażenia. Przedstawiony poniżej typowy przykład wykonuje przypisanie wyniku funkcji readLine do zmiennej line oraz jej porównanie z wartością nuli (która oznacza osiągnięcie końca strumienia)15. 1f ((line * input.readLineO) — return errors:

null) [...]

A Konieczne jest ujęcie operacji przypisania w nawiasy, tak jak w ma to miejsce w dwóch przedstawionych przykładach. Operatory porównania używane w połączeniu z przy­ pisaniami zwykle mają wyższy priorytet, stąd poniższe wyrażenie: c - getopt (argc. argv. "t:") !■ -1

jest interpretowane jako: c - (getopt (argc. argv. "t:”) !- -1)

co oznacza przypisanie do zmiennej c wyniku porównania wartości zwracanej przez

A funkcję getopt z wartością 1, zamiast przypisania do niej wartości zwracanej przez getopt. Ponadto, zmienna użyta w celu przypisania wyniku wywołania funkcji powinna

umożliwiać przechowywanie zarówno normalnych wartości zwracanych przez funkcję, jak i wszelkich wartości wyjątkowych, wskazujących na wystąpienie błędu. Stąd też zwykle funkcje, które zwracają znaki, takie jak getopt lub getc, i które mogą również zwracać wartość błędu, taką jak •1 lub EOF, swoje wyniki przechowują w zmiennych |T | całkowitych, a nie zmiennych znakowych w celu umożliwienia przechowywania nadzbioru wszystkich znaków oraz wartości wyjątkowych (listing 2.3:7). Poniżej przedstawio­ no kolejny przykład typowego wykorzystania tej samej konstrukcji, która kopiuje znaki ze strumienia plikowego pf do strumienia plikowego active do momentu, aż zostanie osiągnięty koniec strumienia pf16. while ((c - getc(pf)) !- EOF) putc(c. active):

A

Treść instrukcji whi le może być pojedynczym poleceniem lub blokiem, który stanowi jedna lub więcej instrukcji ujętych w nawiasy klamrowe. Podobnie jest w przypadku wszystkich poleceń sterujących przebiegiem programu, czyli if , do, for oraz switch. Programy zwykle zawierają wcięcia odpowiednich wierszy kodu źródłowego, uwidaczniające polecenia tworzące część instrukcji sterującej. Jednak wcięcia stanowią jedynie wizualną podpowiedz dla czytającego kod. Jeżeli nie zostaną zastosowane nawiasy, sterowanie będzie dotyczyć tylko jednego polecenia występującego tuż za odpowiednią instrukcją sterującą, bez względu na zastosowany schemat wcięć. Przykładowo, poniż­ szy kod nie wykonuje działań, które sugerowałyby wykorzystane wcięcia1 .

15cocoon/src/java/org/apache/cocoon/components/language/programming/java/Javacjava: 106 - 112. 16netbsdsrc/usr.bin/m4/eval.c: 601 - 602. 17netbsdsrc/usr.sbin/timed/'timed/timed, c: 564 - 568.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

45

for (ntp - nettab: ntp !- NULL; ntp - ntp->next) | 1f (ntp->status — MASTER) rmnetmachs(ntp): ntp->status - NOMASTER:

} Wiersz ntp->status = NOMASTER; jest wykonywany w każdej iteracji pętli for, a nie tylko wówczas, gdy warunek instrukcji if jest spełniony. Ćwiczenie 2.9. Sprawdź, w jaki sposób edytor, którego używasz, potrafi identyfikować odpowiadające sobie pary nawiasów otwierających i zamykających. Jeżeli tego nie potrafi, warto rozważyć możliwość sięgnięcia po inne narzędzie. Ćwiczenie 2.10. Kod źródłowy programu expand zawiera pewne nadmiarowe nawiasy klamrowe. Odszukaj je. Sprawdź wszystkie struktury sterujące, które nie wykorzystują nawiasów klamrowych i oznacz polecenia, które są wykonywane. Ćwiczenie 2.11. Sprawdź, czy wcięcia zastosowane w kodzie programu expand od­ powiadają przebiegowi jego wykonania. Postąp podobnie w przypadku programów używanych we własnym środowisku. Ćwiczenie 2.12. Język Perl nakazuje użycie nawiasów klamrowych w przypadku wszyst­ kich struktur sterujących. Skomentuj wpływ takiego rozwiązania na czytelność kodu programów napisanych w języku Perl.

2.4. Instrukcja switch Normalne wartości zwracane przez funkcję getopt są przetwarzane w ramach instruk­ cji switch. Są one używane w przypadkach, gdy przetwarzaniu podlega wiele różnych wartości całkowitych lub znakowych. Kod służący do obsługi każdej wartości jest poprzedzony etykietą case. Kiedy wartość wyrażenia w instrukcji switch odpowiada wartości jednej z etykiet case, program rozpoczyna wykonywanie instrukcji od tego miejsca. Jeżeli żadna z wartości etykiet nie odpowiada wartości wyrażenia, ale istnieje etykieta default, sterowanie przechodzi do tego miejsca. W przeciwnym razie nie zoA staje wykonany żaden kod występujący w bloku instrukcji switch. Należy pamiętać, że dodatkowe etykiety napotkane po przejściu sterowania do odpowiedniej etykiety nie powodują zakończenia wykonywania poleceń zawartych w bloku instrukcji switch; w celu zakończenia przetwarzania kodu znajdującego się w bloku switch i kontynu[T| owania działań od instrukcji znajdujących się poza nim. należy wykorzystać instrukcję break. Rozwiązanie to stosuje się często w celu grupowania razem etykiet case, łącząc wspólne elementy kodu. W naszym przypadku, kiedy funkcja getopt zwraca wartość 't', zostają wykonane instrukcje obsługujące ten przypadek, zaś polecenie break po­ woduje przejście sterowania tuż za zamykający nawias klamrowy bloku switch (listing 2.3:4). Ponadto można zauważyć, że kod dla obsługi etykiety default oraz dla zwraca­ nej wartości błędu ' ?' jest wspólny, gdyż obie etykiety są zgrupowane razem.

A Kiedy kod danej etykiety case lub defaul t nie jest zakończony instrukcją powodującą przejście sterowania poza blok switch (na przykład break, return lub continue), pro­ gram kontynuuje wykonywanie instrukcji występujących po następnej z kolei etykiecie.

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

46

Badając kod. należy wypatrywać tego rodzaju błędów. Tylko w rzadkich przypadkach [~i~l programista może faktycznie chcieć zapewnić takie zachowanie programu. W celu poin­ formowania osób konserwujących kod o takiej sytuacji, często oznacza się te miejsca odpowiednim komentarzem, na przykład FALLTHROUGH, jak w poniższym przykładzie18. case 'a': fts options |- FTS_SEEOOT: /* FALLTHROUGH */ case 'Afjlstdot - 1: break:

Powyższy kod pochodzi z fragmentu przetwarzania opcji uniksowego polecenia ls, które służy do wyświetlania listy plików w katalogu. Opcja -A określa uwzględnienie w liście plików, których nazwy rozpoczynają się od znaku kropki (są to, zwyczajowo, pliki ukryte), natomiast opcja -a nieco modyfikuje to zachowanie poprzez dodanie do listy dwóch wpisów dotyczących katalogów1'. Programy, które automatycznie wery­ fikują kod źródłowy pod względem występowania częstych błędów, takie jak polecenie uniksowe 1int, mogą wykorzystywać komentarz FALLTHROUGH w celu wyeliminowania niepotrzebnych ostrzeżeń.

A Instrukcja switch niezawierająca etykiety default niejawnie ignoruje niespodziewane wartości. Nawet jeśli wiadomo, że tylko ustalony zbiór wartości będzie przetwarzany przez instrukcję switch, dobrą praktyką programistycznąjest zawarcie etykiety defaul t. P I Taka etykieta może służyć do wyłapywania błędów programistycznych, które dają nie­ spodziewane wartości i alarmowania osoby konserwującej program, jak w poniższym przykładzie’0. switch (program) { case ATO;

[...] case BATCH: writef1le(time(NULL). 'b‘ : break: default: panicC Interna 1 error"); break;

) W naszym przypadku instrukcja switch może obsłużyć dwie wartości zwracane przez funkcję getopt. 1. Zostaje zwrócone 't' w celu obsłużenia opcji -t. Zmienna optarg wskazuje na argument opcji -t. Przetwarzanie jest wykonywane poprzez wywołanie funkcji getstops ze specyfikatorem tabulacji jako argumentem. 2. Zostaje zwrócone ' ? ', kiedy funkcja getopt wykryje nieznaną opcję lub inny błąd. W takim przypadku funkcja usage drukuje informacje o użyciu programu oraz wychodzi z niego.

18 netbsdsrc/bin/ls/ls.c: 173- 178. 19Chodzi tu o katalog bieżący oznaczany symbolem . oraz katalog nadrzędny oznaczany symbolem — przyp. tłum. 20 netbsdsrc/usr.bin/at/at.c: 535 - 561.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

47

Instrukcja swi tch jest również używana jako część pętli przetwarzania znaków programu (listing 2.3:7). Każdy znak jest sprawdzany i niektóre z nich (znak tabulacji, nowego wiersza oraz cofnięcia) są poddawane specjalnemu przetwarzaniu. Ćwiczenie 2.13. Kod instrukcji swi tch jest formatowany w sposób odmienny od innych instrukcji. Określ wykorzystywane reguły formatowania i wyjaśnij zasadność ich sto­ sowania. Ćwiczenie 2.14. Zbadaj sposób obsługi niespodziewanych wartości w instrukcjach swi tch w czytanych programach. Zaproponuj zmiany służące wykrywaniu błędów. Przeanalizuj, w jaki sposób zmiany te wpływają na solidność programów w środowi­ sku produkcyjnym. Ćwiczenie 2.15. Czy w Twoim środowisku istnieje narzędzie lub opcja kompilatora umożliwiająca wykrywanie brakujących instrukcji break w kodzie polecenia swi tch? Skorzystaj z niej i sprawdź wyniki na przykładowych programach.

2.5. Pętle for W celu pełnego zrozumienia sposobu przetwarzania przez program expand opcji po­ dawanych w wierszu poleceń, musimy teraz zbadać funkcję getstops. Chociaż rola pełniona przez jej jedyny argument cp nie jest oczywista na podstawie nazwy, kiedy zbadamy, w jaki sposób funkcja getstops jest używana, wszystko stanie się jasne. Przekazywany do niej jest argument opcji -t, czyli lista tabulatorów, na przykład 4, 8, 16. 24. Strategie określania zadań pełnionych przez funkcje omówione w podrozdziale 2.2 można wykorzystać względem ich argumentów. Powoli zaczyna się zatem kształ­ tować wzorzec czytania kodu. Czytanie kodu to zadanie związane ze stosowaniem wielu alternatywnych strategii: analizy wstępującej lub zstępującej, użycia technik heurystycz­ nych lub przeglądania komentarzy i zewnętrznej dokumentacji — wszystkie one po­ winny być wypróbowane, jeśli wymaga tego dany problem. Po ustawieniu wartości zmiennej nstops na 0, funkcja getstops wchodzi do pętli for. Zazwyczaj pętlę for określa wyrażenie obliczane przed rozpoczęciem działania pętli, wyrażenie obliczane przed każdą iteracją w celu określenia, czy powinno nastąpić kolejne wykonanie treści pętli oraz wyrażenie obliczane po zakończeniu wykonywania treści pętli. Pętle for są często używane w celu wykonania fragmentu kodu określoną ilość razy21. for (1 * 0; 1 < len. 1++) { |T] Tego rodzaju pętle bardzo często występują w programach. Należy je odczytywać jako „wykonaj treść kodu len razy”. Z drugiej strony, wszelkie odstępstwa od tego stylu, na przykład użycie wartości początkowej różnej od 0 lub operatora porównania innego niż orientation].

41 netbsdsrc/games/worms/worms.c: 419. 42 netbsdsrc/bin/csh/set.c: 852.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

59

normal )

) ))[w->onentation]:

Odczytywanie wyrażenia warunkowego w jego rozwiniętej formie jest bez wątpienia łatwiejsze, jednak wciąż istnieją możliwości wprowadzenia ulepszeń. W tym momencie możemy stwierdzić, że zmienne x i y, które sterują obliczeniami wyrażenia, są porów­ nywane z trzema różnymi wartościami: 1. 0 (wyrażone jako !x lub !y). 2 . bottom lub last. 3. Wszystkie inne wartości.

Możemy więc przepisać wyrażenie, formatując je, jako serię kaskadowych instrukcji if-else (wyrażonych przy użyciu operatora ?:) w celu zademonstrowania tego faktu. Otrzymany rezultat można zobaczyć na listingu 2.8 (po prawej). Wcięcia, którymi opatrzono wyrażenie, stają się teraz oczywiste: programista wybiera jedną spośród dziewięciu różnych wartości lokalizacji w oparciu o połączone wartości zmiennych x i y. Oba alternatywne rozwiązania podkreślają wizualne znaczenie inter­ punkcji za cenę zawartości semantycznej i zajmują dużą powierzchnię. Mimo wszystko, biorąc pod uwagę nasze nowo odkryte zależności, możemy utworzyć dwuwymiarową tablicę zawierającą te wartości lokalizacji, indeksując ją przesunięciami otrzymanymi z wartości x i y. Nowy rezultat przedstawiono na listingu 2.9. Warto zauważyć, w jaki sposób w trakcie inicjalizacji tablicy o nazwie locations jest wykorzystywana dwuwy­ miarowa struktura tekstowa, co ilustruje dwuwymiarowy charakter wykonywanych obliczeń. Wartości inicjalizacji rozmieszczono w tekście programu w postaci dwuwy­ miarowej, tablica jest indeksowana w zwykle niestandardowym porządku [y][x], zaś odwzorowanie dotyczy liczb całkowitych „0, 2, 1”, a nie narzucających się „0, 1, 2”, tak aby dwuwymiarowa prezentacja współgrała ze znaczeniem wyrazów upleft. (lewy górny), upper (górny) itd. Listing 2.9. Kod wykrywania lokalizacji, zastępujący wyrażenie warunkowe_____________________ struci options *locations[ 3 ][ 3 ] - { (upleft. upper. uprlght}. • (left. normal. rlght).

{lowleft. Iower. Iownght} i. /• int xlocat1on. ylocatlon. • ----1 f C x — 0) xlocat1on else if (x — xlocation else xlocat1on

• -----------

Odwzorowanie lokalizacji

Do przechowywania przesunięć odwzorowania dla x o ra zy Określenie przesunięcia x

- 0; last) - 2: - 1:

if (y — 0) • --------- --- Określenie przesunięcia y ylocatlon - 0: else 1f (y — bottom) ylocatlon - 2:

60

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

else ylocatlon - 1 :

op - &(locations[ylocation][xlocation])[w->orientation]:

Otrzymany kod, liczący 20 wierszy, jest dłuższy od oryginalnego jednowierszowego, ale okazuje się o 7 wierszy krótszy od czytelnej reprezentacji w formie kaskadowych instrukcji lf-else owej jednowierszowej instrukcji. Wydaje się bardziej czytelny, jest samodokumentujący się i łatwiej można zweryfikować jego poprawność. Można by wysunąć argument, że oryginalna wersja jest wykonywana szybciej niż nowa. Byłby on jednak oparty na fałszywym założeniu, że czytelność kodu i jego wydajność są sprzecz­ nościami. Nie ma żadnego powodu, aby poświęcać czytelność kodu na rzecz wydaj­ ności. Choć jest prawdą, że wydajne algorytmy i pewne techniki optymalizacji mogą komplikować kod, a przez to utrudnić jego analizę, nie oznacza to, że zapewnienie zwięzłości i wysokiego stopnia skomplikowania kodu zapewni zwiększenie jego wy­ dajności. W systemie Autora wersja oryginalna i nowa były wykonywane w dokładnie takim samym czasie: 0,6 ps. Nawet gdyby wystąpiły różnice w szybkości działania, analiza kosztów związanych z konserwacją oprogramowania, placami programistów oraz wydajnością jednostki centralnej zwykle nakazuje przywiązywać większą wagę do czytelności niż wydajności działania kodu. Jednak nawet kod z listingu 2.9 można postrzegać jako nie do końca zadowalający: osiągamy dzięki niemu pewne korzyści za cenę dwóch wad. Po pierwsze, rozdziela on kod na dwie części, które, choć na listingu są przedstawione razem, w prawdziwym kodzie musiałyby zostać odseparowane. Po drugie, wprowadza dodatkowe kodowanie (0, 1, 2), tak więc zrozumienie działania kodu wymaga dwóch analiz zamiast tylko jednej (odwzoruj „0, last, other” na „0, 2, 1”, a następnie odwzoruj parę wartości „0, 2, 1” na jeden z dziewięciu elementów). Pojawia się pytanie, czy istnieje możliwość bezpośredniego wprowadzenia dwuwymiarowej struktury wykonywanych obliczeń do kodu warunkowego. Poniższy fragment kodu43 to ponownie wyrażenia warunkowe, które jednak zostały z rozwagą rozmieszczone tak, aby wyrazić ceł wykonywanych obliczeń. op -

&( !y ? (!x ? upleft : x!-last ? upper upnght ) . y!-bottom ? (!x ? left : x!-last ? normal right ) (!x ? Iowleft : x!«last ? lower : Iowright ) )[w->orientationJ:

Powyższy zapis stanowi doskonały przykład na to, w jaki sposób kreatywne rozmiesz­ czenie kodu może czasem być wykorzystane do zwiększenia jego czytelności. Należy zauważyć, że dziewięć wartości wyrównano do prawej strony w trzech kolumnach, co pozwoliło na ich uwydatnienie i wykorzystanie powtórzeń fragmentów „left” oraz „right” w ich nazwach. Należy również zauważyć, że powszechnie stosowana praktyka umiesz­ czania znaków spacji wokół operatorów została naruszona w przypadku operatora != w celu zredukowania wyrażeń sprawdzających do postaci wizualnych znaczników, przez co dziewięć wartości danych wyróżnia się jeszcze bardziej. Wreszcie fakt, że całe wyrażenie mieści się w pięciu wierszach sprawia, że pionowe wyrównanie pierwszego i ostatniego nawiasu wyraźnie pokazuje, że podstawowa struktura całej instrukcji ma postać: 43

Zasugerowany przez Guya Steele'a.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

61

op - &( )[w->orientation];

Wybór między dwiema nowymi alternatywnymi reprezentacjami jest w dużej mierze kwestią gustu, jednak prawdopodobnie nie udałoby się nam opracować drugiego zapi­ su bez wyrażenia najpierw kodu w pierwszej, bardziej opisowej i jawnej postaci. Wyrażenie, które zmodyfikowaliśmy, było bardzo długie i oczywiście nieczytelne. W mniej ewidentnych przypadkach można jednak również odnieść spore korzyści dzięki przepisaniu kodu. Często wyrażenie można uczynić bardziej czytelnym poprzez dodanie białych znaków, podzielenie go na mniejsze części przy użyciu zmiennych tymczasowych lub użycie nawiasów w celu podkreślenia kolejności wykonywania określonych działań. Nie zawsze konieczne jest zmienianie struktury programu w celu zwiększenia jego czytelności. Często elementy, które nie wpływają na działanie programu (takie jak komentarze, użycie białych znaków oraz wybór nazw zmiennych, funkcji i klas) może mieć wpływ na czytelność programu. Weźmy pod uwagę wysiłek, jaki włożyliśmy w zrozumienie kodu funkcji getstops. Zwięzły komentarz umieszczony przed jej defi­ nicją znacznie zwiększyłby czytelność programu. /*

* Analiza i weryfikacja poprawności specyfikacji pozycji tabulatorów, na którą * wskazuje zmienna cp poprzez ustawienie zmiennych globalnych nstops oraz tabstops[] * Wyjście z programu z komunikatem o błędzie w przypadku niepoprawnej specyfikacji

*/ Czytając kod znajdujący się pod naszą kontrolą, warto nabrać nawyku dodawania komentarzy tam, gdzie to konieczne. W podrozdziałach 2.2 oraz 2.3 wyjaśniliśmy, w jaki sposób nazwy i wcięcia mogą zapewnić podpowiedzi co do funkcjonalności kodu. Niestety, czasem programiści wybierają nazwy, które nie są w niczym pomocne i niekonsekwentnie używają w swo­ ich programach schematu wcięć. Można poprawić czytelność słabo zapisanego kodu dzięki lepszej organizacji wcięć i przemyślanemu doborowi nazw zmiennych. Tego rodzaju działania to ostateczność: należy je wykorzystywać tylko wówczas, gdy posiada się pełną kontrolę nad kodem źródłowym, jest się pewnym, że wprowadzone zmiany będą o wiele lepsze od oryginału oraz będzie można powrócić do oryginalnego kodu, jeżeli coś pójdzie nie tak. Użycie systemu zarządzania wersjami, takiego jak Revision Control System (RCS), Source Code Control System (SCCS), Concurrent Versions System (CVS) lub Visual SourceSafe firmy Microsoft, może pomóc kontrolować mo­ dyfikacje wprowadzane w kodzie. Przyjęcie określonego stylu nazewnictwa zmiennych oraz stosowanych wcięć może wydawać się nużącym zadaniem. Modyfikując kod, który stanowi część większego systemu, w celu zwiększenia jego czytelności należy postarać się zrozumieć i zaadaptować konwencje wykorzystywane w pozostałych partiach kodu (patrz rozdział 7.). Wiele przedsiębiorstw posiada określone style kodowania. Należy się ich nauczyć i starać się stosować. W przeciwnym wypadku należy przyjąć jeden standardowy styl (na przykład używany przez grupy GNU lub BSD45) i konsekwent­ nie go stosować. Kiedy wcięcia tekstu programu są stosowane niekonsekwentnie i nie można tego ręcznie poprawić, pomocne może okazać się któreś z wielu narzędzi (takich 44http://www.gnu.org/prep/standardsjoc. html. 45

netbsdsrc/share/misc/style: 1 -315.

62

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

A jak indent) automatycznie poprawiających wcięcia (patrz podrozdział 10.7). Narzędzi takich należy jednak używać ostrożnie: swobodne użycie białych znaków pozwala pro­ gramistom na przekazywanie wizualnych podpowiedzi, które wykraczają poza moż­ liwości zautomatyzowanych narzędzi formatujących. Wykorzystanie programu indent względem kodu z listingu 2.9 bez wątpienia zmniejszyłoby jego czytelność.

A Trzeba pamiętać, że choć poprawa wcięć treści kodu może pomóc w zakresie czytelności, to jednocześnie zaburza historię zmian programu w systemie kontroli wersji. Z tego względu prawdopodobnie najlepszym rozwiązaniem jest niełączenie ponownego for­ matowania z jakimikolwiek faktycznymi zmianami w logice programu. Należy wyko­ nać przeformatowanie, sprawdzić je, a dopiero potem wprowadzić pozostałe zmiany. W ten sposób przyszli czytelnicy kodu będą w stanie selektywnie określać i analizować wprowadzone zmiany w logice programu bez przeglądania niepotrzebnych informacji o globalnych zmianach formatowania. Z drugiej strony, kiedy poznaje się historię wersji programu, która zawiera wykonanie globalnej zmiany schematu wcięć, przy użyciu programu diff, często można uniknąć problemów związanych z wprowadzonymi zmia­ nami dzięki określeniu opcji -w, nakazującej programowi d iff ignorowanie różnic pod względem białych znaków. Ćwiczenie 2.25. Znajdź w swoim środowisku pracy lub na płycie CD-ROM dołączonej do książki pięć przykładów, w których struktura kodu może zostać poprawiona w celu zwiększenia jej czytelności. Ćwiczenie 2.26. Można znaleźć dziesiątki celowo zapisanych w nieczytelnej postaci programów w C wśród zasobów witryny International Obfuscated C Code Contest46. Większość z nich wykorzystuje kilka poziomów zaciemniania w celu ukrycia algo­ rytmów. Sprawdź, w jaki sposób stopniowe zmiany kodu mogą pomóc w rozwikłaniu takiego kodu. Jeżeli nie znasz preprocesora języka C. unikaj programów zawierających dużą liczbę wierszy #defi ne. Ćwiczenie 2.27. Zmodyfikuj kod lokalizacji pozycji, który badaliśmy, tak aby praco­ wać na obrazie lustrzanym (zamień stronę prawą z lewą). Zmierz, ile czasu zajmie Ci wprowadzenie zmian w oryginalnym kodzie oraz wersji końcowej przedstawionej na listingu 2.9. Nie patrz na opracowane przez nas czytelne reprezentacje. Gdyby miały się okazać pomocne, opracuj je od początku. Oszacuj różnicę kosztów, przyjmując obo­ wiązujące stawki za pracę programisty. Jeżeli czytelny kod działa dwa razy wolniej od oryginalnego kodu (w rzeczywistości tak nie jest), oblicz koszt związany z tym spowol­ nieniem przyjmując rozsądne założenia co do liczby powtórzeń wykonania kodu na komputerze kupionym za określoną cenę. Ćwiczenie 2.28. Jeżeli nie znasz żadnego określonego standardu zapisu kodu. posta­ raj się znaleźć informacje o jednym z takich standardów i zastosować go. Zweryfikuj lokalny kod pod względem jego występowania w plikach źródłowych.

46 http://www.ioccc.org.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

63

2.10. Pętle do i wyrażenia całkowite Nasze zrozumienie działania programu expand możemy uzupełnić, kierując uwagę na kod, który wykonuje właściwe przetwarzanie (listing 2.3, strona 39). Rozpoczyna się on od pętli do. Treść pętli do jest wykonywana co najmniej raz. W naszym przypadku jest ona wykonywana dla każdego wciąż nieprzetworzonego argumentu. Argumenty mogą określać nazwy plików, których zawartość ma zostać poddana przetworzeniu. |T| Kod przetwarzający argumenty z nazwami plików (listing 2.3:6) ponownie otwiera strumień plikowy stdin w celu uzyskania dostępu do każdego kolejnego argumentu nazwy pliku. Jeżeli nie zostaną podane żadne argumenty z nazwami plików, treść in­ strukcji if (listing 2.3:6) nie zostanie wykonana i program expand przetworzy swoje standardowe wejście. Faktyczne przetwarzanie jest związane z czytaniem znaków i ślel~i~l dzeniem pozycji bieżącej kolumny. Instrukcja switch, bardzo często używana w przy­ padku przetwarzania znaków, obsługuje wszystkie znaki, które mają wpływ na pozy­ cję kolumny w specjalny sposób. Nie będziemy badać szczegółowo logiki związanej z pozycjonowaniem tabulatorów. Łatwo zauważyć, że pierwsze trzy i ostatnie dwa bloki ponownie mogą zostać zapisane w postaci sekwencji kaskadowych instrukcji if-else. Naszą uwagę skupimy na pewnych wyrażeniach występujących w kodzie.

A Czasem sprawdzenia równości, takie jak używane w przypadku zmiennej

nstops (na przykład nstops == 0), są mylnie zapisywane przy użyciu operatora przypisania = zamiast operatora równości W językach C, C++ oraz Perl instrukcja podobna do poniższej4':

if ) q[-l] - 'Sn': wykorzystuje poprawne wyrażenie sprawdzające dla instrukcji if. przypisując wartość q do p i sprawdzając, czy wynik jest równy zero. Jeżeli programista zamierzałby spraw­ dzać p względem q, większość kompilatorów nie wygenerowałaby żadnego komunikatu [Tl o błędzie. W opisywanym poleceniu nawiasy w wyrażeniu (p = q) mają prawdopodobnie na celu podkreślenie, że intencją programisty rzeczywiście jest przypisanie, a następ[T1 nie porównanie z wartością zero. Jednym ze sposobów zapobiegania wszelkim wątpliwo­ ściom w takim przypadku jest jawne dokonanie porównania z wartością NULL48. if C(p - strchriname.

!- NULL) {

p++:

W tym przypadku porównanie można by również zapisać jako if (p = strchr(name. '= ' )), jednak nie byłoby wówczas wiadomo, czy jest to przypisanie celowe, czy po­ myłka. |71 W końcu, kolejnym podejściem, z jakim można się spotkać, jest przyjęcie stylu, według którego wszystkie porównania ze stałymi są zapisywane ze stałą umieszczoną po lewej r » 4 4 strome wyrażenia porównania . 1f (0 — serconsole) serconslnit - 0:

4 netbsdsrc/bin/ksh/history.c: 313-314. 48nelbsdsrc/bin/sh/var.c: 507 - 508. 49netbsdsrc/sys/arch/amiga/dev/ser.c\ 227 - 228.

64

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Kiedy używany jest taki styl. omyłkowe przypisania do stałych zostają oznaczone przez kompilator jako błędy. W przypadku programów napisanych w języku Java lub C# istnieje mniejsze prawdo­ podobieństwo napotkania tego rodzaju błędów, gdyż języki te jako wyrażenia sterujące w odpowiednich instrukcjach sterujących akceptują tyłko wartości logiczne. W rzeczy­ wistości Autorowi nie udało się znaleźć nawet jednej podejrzanej instrukcji w kodzie Java znajdującym się na płycie CD-ROM dołączonej do książki. Wyrażenie col umn & 7 użyte w celu sterowania pierwszą pętlą do w kodzie przetwarza­ nia pętli również jest interesujące. Operator &wykonuje operację bitowego iloczynu logicznego na swoich dwóch operandach. W naszym przypadku nie interesują nas bity. jednak zerując najbardziej znaczące bity zmiennej column operacja ta zwraca resztę [Tl z dzielenia col umn przez 8. Wykonując operacje arytmetyczne, należy odczytywać zapis a & b jako a * (b + 1), kiedy b = 2" - 1. Intencją zapisu wyrażenia w ten sposób jest zastąpienie dzielenia bitowym iloczynem logicznym — czasem wykonywanym wydaj­ niej. W praktyce współczesne kompilatory optymalizujące potrafią rozpoznawać takie sytuacje i samodzielnie dokonywać takich podstawień, a jednocześnie różnica w szyb­ kości działania operacji dzielenia i bitowego iloczynu logicznego w przypadku współ­ czesnych procesorów nie jest tak duża, jak miało to miejsce niegdyś. Dlatego też należy nauczyć się czytania kodu zawierającego podobne sztuczki, ale unikać ich stosowania. Istnieją dwa inne często występujące przypadki, w których instrukcje bitowe są uży­ wane w zastępstwie instrukcji arytmetycznych. Chodzi tu o operatory przesunięcia « oraz » , które przesuwają bity wartości całkowitej w lewo lub w prawo. Ze względu na fakt, że każda pozycja bitowa wartości całkowitej ma wartość równą potędze liczby 2. przesunięcie tej wartości daje ten sam efekt, co pomnożenie lub podzielenie jej przez kolejną potęgę liczby 2 równą liczbie pozycji, o jaką następuje przesunięcie. Dlatego też w kontekście arytmetycznym operatory przesunięcia można traktować następująco: |T |

♦ Zapis a « n należy odczytywać jako a * k, gdzie k = 2". Poniższy przykład wykorzystuje operator przesunięcia w lewo w celu wykonania mnożenia przez 450. n - ((dp - cp) «

[Tl

2) + 1; /* razy 4 + 1 */

♦ Zapis a » n należy odczytywać jako a / k, gdzie k = 2". Poniższy przykład pochodzący z podprogramu wyszukiwania binarnego wykorzystuje operator przesunięcia w prawo w celu wykonania dzielenia przez 251. bp ■ bpi + ((bp2 - bpi) »

1):

A Trzeba pamiętać, że logiczny operator przesunięcia w prawo języka Java »> nie powi­ nien być używany do wykonywania operacji dzielenia arytmetycznego na wartościach ze znakiem, gdyż daje błędne wyniki w przypadku, gdy zostanie zastosowany względem liczb ujemnych.

' ü netbsdsrc/bin/csh/str. c: 460. 51 netbsdsrc/bin/csh/func.c: 106.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

65

Ćwiczenie 2.29. Większość kompilatorów zapewnia mechanizm podglądu skompilo­ wanego kodu w języku asemblera. Dowiedz się, w jaki sposób można wygenerować kod asemblera podczas kompilacji programu napisanego w języku C w Twoim śro­ dowisku i zbadaj kod wygenerowany przez swój kompilator dla pewnych przykładów wyrażeń arytmetycznych oraz odpowiadających im wyrażeń używających instrukcji bitowych. Wypróbuj różne poziomy optymalizacji oferowane przez kompilator. Sko­ mentuj czytelność i wydajność działania kodu w obu alternatywnych przypadkach. Ćwiczenie 2.30. Jakiego rodzaju argument mógłby spowodować niepowodzenie wyko­ nania programu expandí W jakich warunkach mógłby zostać podany taki argument? Zaproponuj proste rozwiązanie tego problemu.

2.11. Podsumowanie wiadomości o strukturach sterujących Po zbadaniu szczegółów składniowych dotyczących instrukcji sterujących przebiegiem programu, możemy skupić naszą uwagę na sposobie analizowania ich na poziomie abstrakcyjnym. Pierwsze, o czym trzeba pamiętać, to zasada, że należy badać tylko jedną strukturę sterującą na raz i traktować jej treść jak czarną skrzynkę. Piękno programowania struk­ turalnego polega na tym, że wykorzystane struktury sterujące pozwalają na abstrahowa­ nie i selektywne analizowanie fragmentów programu bez zagrożenia ze strony wysokiego poziomu ogólnej złożoności całego programu. Rozważmy poniższy fragment kodu12: while (enum.hasMoreElementsO) { [ . . . ]

1f (object instanceof Resource) ( [ . . . ]

1f (Icopyds. os)) [ . . . ]

) else 1f (object instanceof InputStream) ( 1f max) max - depthsin];

} Jeżeli zdefiniujemy nO jako liczbę elementów w tablicy depths (początkowo przecho­ wywaną w zmiennej n), możemy formalnie wyrazić wynik, którego oczekujemy po zakończeniu działania pętli jako: max = maximum {depths[0 : n0)}

Używamy zapisu [a : b) w celu określenia przedziału zawierającego a. ale kończącego się jeden element przed b, to znaczy, [a : b - 1], Odpowiednim niezmiennikiem może wówczas być: max = maximum{depths[n :nn)}

Niezmiennik zostaje określony po pierwszym przypisaniu do zmiennej max, tak więc jest zachowany na początku pętli. W momencie zmniejszenia wartości zmiennej n nie zawsze jest on spełniony, gdyż przedział [n : n0) zawiera element o indeksie n, który może być większy od wartości maksymalnej przechowywanej w max. Niezmiennik zostaje określony ponownie po wykonaniu instrukcji if, która dostosowuje wartość max, jeżeli wartość nowego elementu właśnie rozszerzonego przedziału jest większa od maksimum przechowywanego do tej pory. Pokazaliśmy zatem, żc niezmiennik jest zawsze spełniony na końcu każdej iteracji pętli, a stąd będzie prawdziwy również po jej zakończeniu. Ze względu na fakt, że pętla zostanie zakończona, kiedy zmienna n (którą można traktować jako zmiennik pętli) osiągnie wartość 0, nasz niezmiennik może w tym momencie zostać przepisany w postaci oryginalnej specyfikacji, którą chcieliśmy speł­ nić, co pokazuje, że pętla faktycznie osiąga poszukiwany przez nas wynik. Takie rozumowanie możemy zastosować względem przykładu wyszukiwania binarnego. Na listingu 2.11 przedstawiono ten sam algorytm w nieco przeredagowanej postaci, tak aby uprościć argumentację dotyczącą niezmiennika. ♦ Zastąpiliśmy operator przesunięcia w prawo » dzieleniem. ♦ Pominęliśmy zmienną size, gdyż jest ona używana jedynie w celu symulowania arytmetyki wskaźników bez konieczności określenia typu wskaźnika. ♦ Przenieśliśmy ostatnie wyrażenie instrukcji for na koniec pętli w celu wyraźniejszego zaprezentowania kolejności wykonywania działań w pętli.

55 XFree86-3.3/xc/lib/Xt/GCManager.c; 252 - 256.

Rozdział 2. ♦ Podstawowe konstrukcje programistyczne

69

Listing 2.11. Zachowywanie niezmiennika wyszukiwania binarnego const char *base - baseO;• — [1] R należy do (base, bose + nmemb) (lim • nmemb: J im !■0 ;) { i-----------/ 2] R należy do [base, base + lim)

register

fo r

p - base *■ lim / 2: cmp - (*compar)(key. p). 1f (cmp — 0) return ((void *)p). 1f (cmp > 0) J

/*Key > p: moveright * / • -------- (3] R należy do (p. base + lim) R należy do (p + I. base + lim) base • p ♦ 1; • ----------------------- (4 / base = base_old + lim / 2 + / b a s e o ld = base - lim / 2 - 1 R należy do (base, base old + lim) R należy do (base, base - lim / 2 - 1 + hm) 11m— ; • ----------------------------------- [5] R należy do /base, base - (hm + l) / 2 - I + lim R należy do (base, base + lim - (lim + 1) / 2) R należy do [base, bose + lim / 2) ) /* else move left * / • ---------------(6J R należy do (base.p) R należy do (base, base + lim / 2) 11m /■ 2, (7 / R należy do (base, base + lim)

) return (NULL):

Odpowiednim niezmiennikiem może być zauważenie, że wartość, której szukamy, leży w określonym przedziale. Będziemy używać zapisu R e [a : b) w celu oznaczenia, że rezultat wyszukiwania leży między elementami tablicy: a (wraz z nim) a b (bez niego). Ze względu na fakt, że base i 1im są używane w pętli w celu ograniczenia przedziału wyszukiwania, naszym niezmiennikiem będzie R e [base : base + lim). Udowodni­ my, że funkcja bsearch faktycznie znajduje wartość w tablicy, jeżeli taka wartość ist­ nieje, by pokazać, że niezmiennik jest zachowywany po każdej iteracji pętli. Ze względu na fakt, że funkcja compar jest zawsze wywoływana z argumentem należą­ cym do zakresu niezmiennika (base + 1im / 2) oraz wartość 1im (zmiennika) jest dzielo­ na przez 2 w każdej iteracji pętli, możemy być pewni, że funkcja compar w końcu zlo­ kalizuje szukaną wartość, o ile ta istnieje. Na początku funkcji bsearch możemy jedynie określić specyfikację funkcji: R e [baseO : baseO + nmemb). Jednakże na podstawie listingu 2.11:1 wyrażenie to możemy przekształ­ cić do postaci R ^ [base : base + nmemb) i po przypisaniu w instrukcji for (listing 2.11:2) jako R e [base : base + lim) — nasz niezmiennik. A zatem ustaliliśmy, że nasz niezmiennik jest zachowany na początku pętli. Wynik funkcji compar jest dodatni, jeżeli wartość, której szukamy jest większa od wartości znajdującej się w punkcie p. Stąd, biorąc pod uwagę listing 2.11:3, możemy stwierdzić, że: R e [p :b a s e + lim ) s / ? e [ p + l : b a s e + lim ) Jeżeli wyrazimy oryginalną wartość base jako baseow, nasz oryginalny niezmiennik, po wykonaniu przypisania z listingu 2.11:4, ma teraz postać: R e [base : base»w+ lim). Zakładając, że wartością p było base + 1im / 2, otrzymujemy:

70

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

b a s e = b a s e ojd_name - estrdup(name): d->d_name[len] - '\0';

[. T

return d;

) HI Kod służący do przydzielenia pamięci równej co do pojemności rozmiarowi konstrukcji s tru c t oraz rzutowanie wyniku na odpowiedni wskaźnik bywają często przenoszone do makra o nazwie takiej jak new (jak adekwatny operator języka C++)2 #define new(type)

(type *) calloctsizeofttype). 1)

[...] node - newtstruct codeword_entry).

3.1.3. Wywołania przez referencję Wskaźniki są również używane w funkcjach, które pobierają argumenty przekazywane przez referencją (ang. by reference). Argumenty funkcji przekazywane przez referencję są wykorzystywane w celu zwracania wyników funkcji lub w celu uniknięcia narzutu czasowego związanego z kopiowaniem argumentu funkcji. Kiedy argumenty typu wskaźnikowego są używane w celu zwrócenia wyników funkcji, w jej treści znajduje się instrukcja przypisania wartości do takich argumentów, jak w przypadku poniższej funkcji, która ustawia wartość gid na identyfikator grupy o podanej nazwie4. int gid nametchar *name. gid t *g1d)

(

[...] *gid * ptr->g1d - gr->gr_gid: return(O);

1 Wartość zwracana przez funkcję jest wykorzystywana jedynie w celu określenia błędu. Wiele funkcji interfejsu AP1 systemu Windows wykorzystuje taką konwencję. Po stro­ nie obiektu wywołującego zwykle widzi się odpowiedni argument przekazywany do funkcji poprzez zastosowanie operatora adresu (&) względem zmiennej. |Tl Argumenty wskaźnikowe są również używane w celu uniknięcia narzutu związanego z kopiowaniem dużych elementów przy każdym wywołaniu funkcji. Takie argumenty to zwykle struktury, rzadziej liczby zmiennoprzecinkowe o podwójnej precyzji. Tablice są zawsze przekazywane przez referencję i w przypadku większości architektur inne proste typy danych wydajniej jest kopiować w momencie wywołania funkcji niż przekazywać je przez referencję. Poniższa funkcja wykorzystuje dwa argumenty typu strukturalnego 2 XFree86-3.3/xc/doc/specs/PEX5/PEX5.1/SI/xrefh: 113. 3 XFree86-3.3/xc/doc/specs/PEX5/PEX5. l/SI/xrefc: 268. 4 nelbsdsrc/bin/pox/cache.c: 430 - 490.

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

76

(now oraz then) przekazywane przez referencję tylko w celu obliczenia wyniku bez mo­

dyfikowania treści struktur. Zapewne zapisano ją w ten sposób w celu uniknięcia narzutu związanego z kopiowaniem struktury timeval przy każdym wywołaniu5. static double diffsectstruct t1meval *now). struct f.1meval *then)

I

return ((now->tv_sec - then->tv_sec)*1.0 + (now->tv usec - then->tv_usec)/1000000,i)>:

1

p~1 We współczesnych programach pisanych w C i C++ często można zidentyfikować argumenty przekazywane przez referencję ze względów wydajnościowych, gdyż są opatrzone deklaratorem const6. static char *ccval (const struct cchar *

Int);

Rola każdego z argumentów jest czasem również identyfikowana poprzez użycie komen­ tarza IN lub OUT, jak w poniższej (zapisanej w stylu sprzed ANSI C) definicji funkcji7: int atmresolve(rt. m. dst. desten) register struct rtentry *rt: struct muf *m. register struct sockaddr *dst; register struct atm_pseudohdr ‘desten:

/* OUT */

{

3.1.4. Uzyskiwanie dostępu do elementów danych W podrozdziale 4.2 zostanie przedstawiony sposób użycia wskaźnika jako kursora bazy danych służącego do uzyskiwania dostępu do elementów tabeli. Można wymienić pew­ ne kluczowe pojęcia pomagające w czytaniu kodu. zawierające wskaźniki służące do uzyskiwania dostępu do tablic. Po pierwsze, wskaźnik na adres elementu tablicy może być wykorzystany do uzyskania dostępu do elementu znajdującego się pod określonym indeksem. Po drugie, arytmetykę wskaźników na elementy tablicy charakteryzują te same cechy semantyczne, co arytmetykę odpowiednich indeksów tablicy. W tabeli 3.1 przedstawiono przykłady wykonywania najczęściej występujących operacji w czasie uzyskiwania dostępu do tablicy a o elementach typu T. Na listingu 3.1''' przedstawiono przykład kodu ze wskaźnikami służącego do uzyskiwania dostępu do stosu. Listing 3.1. Posty) poprzez wskaźnik do stosu opartego na tablicy____________________________ Stackp

* de stack: • -------------- Inicjalizacja stosu

[...] *stackp++ - finchar.



Odloteme

[...] do {

5netbsdsrc/śbin/ping/ping.c: 1028- 1034. 6 nelbsdsrc/bin/slly/print.c: 56. 1 netbsdsrc/sys/netinet/if atm.c: 223 -231. g

netbsdsrc/usr.bin/compress/zopen.c: 523 - 555.

finchar

na stos

Rozdział 3. ♦ Zaawansowane typy danych języka C

77

Tabela 3.1. Kod wykorzystujący indeksy oraz wskaźniki, służący do uzyskiwania dostępu

do tablicy a o elementach typu T Kod z użyciem indeksów

Kod z użyciem wskaźników

T *p = a lub p = &a[0]

int i: i- 0

p

a [ i]

*P

a [ 1 ] .f

p->f

i++

p++

1 +- K 1—N

p

+- K

p — &a[d]

lub p

— a

+N

1f (count-- — 0) return (nura);

*bp++ ■ *--S ta c k p : • ---------------Zdjęcie elementu ze stosu do *bp } w h lle (Stackp > de_stack): • ——Sprawdzenie czy sios jest pusty

3.1.5. Tablice jako argumenty i wyniki

A

A

W programach pisanych w językach C i C++ wskaźniki pojawiają się w przypadku przekazywania tablic do funkcji oraz ich zwracania jako wyników. W kodzie C, kiedy nazwa tablicy zostanie użyta jako argument funkcji, przekazywany jest do niej tylko adres pierwszego elementu tablicy. W rezultacie wszelkie modyfikacje dokonane na danych tablicy w czasie wykonywania funkcji mają wpływ na elementy tablicy przeka­ zanej przez obiekt wywołujący funkcję. Takie niejawne wywołanie przez referencję w przypadku tablic odróżnia je od sposobu przekazywania do funkcji wszystkich innych typów języka C, a przez to może być źródłem niejasności. Podobnie funkcje języka C mogą zwracać jedynie wskaźnik na element tablicy, a nie całą tablicę. A zatem, kiedy funkcja tworzy w tablicy wynik, a następnie zwraca odpo­ wiedni wskaźnik, istotną rzeczą jest zapewnienie, aby tablica nie była zmienną lokalną, której pamięć jest przydzielana na stosie funkcji. W takim przypadku przestrzeń tablicy może zostać nadpisana po zakończeniu działania funkcji i w konsekwencji wynik jej działania będzie niepoprawny. Jednym ze sposobów na uniknięcie tego problemu jest zadeklarowanie takiej tablicy jako statycznej (static), jak w przypadku poniższej funk­ cji, która zamienia adres internetowy na jego reprezentację z wartościami dziesiętnymi rozdzielonymi kropkami ’. char *inet_ntoa(struct in_addr ad)

{ unsigned long int s_ad; 1nt a. b. c. d: static char addr[20]; s_ad - ad.s_addr; d - s ad * 256; s_ad 7- 256. c - s_ad % 256:

netbsdsrcflibexec/identd/identd.c: 120- 137.

78

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source s ad /- 256. b - s.ad * 256; a - s_ad / 256: sprintfOddr. "td.td.td.Sd". a. D. c. d): return addr.

)

A A

Funkcja tworzy wynik w buforze addr. Gdyby nie został on zadeklarowany jako static, jego zawartość (czytelna reprezentacja adresu internetowego) stałaby się nieprawidłowa po zakończeniu działania funkcji. Nawet powyższa konstrukcja nie jest w pełni bezpieczna. Funkcje wykorzystujące globalne lub statyczne zmienne lokalne nie są w większo­ ści przypadków wielobieżne. Oznacza to. że funkcja nie może zostać wywołana z poziomu wątku innego programu, kiedy jedna instancja tej funkcji już działa. Co gorsza, w naszym przypadku wynik funkcji musi zostać zapisany w innym miejscu (na przykład przy użyciu wywołania funkcji strdup) zanim funkcja zostanie wywołana ponownie. W przeciwnym razie zostałby on nadpisany nowym rezultatem. Przykładowo, przed­ stawiona implementacja funkcji inet_ntoa nie mogłaby zostać użyta zamiast funkcji naddr_ntoa w następującym kontekście10: (vo1d)fprintf(ftrace. ”ts Router Ad from Ss to ts via %s life-tdW. act. naddrjitoa(from). naddr_ntoa(to). ifp ? ifp->intjiame : ntohs(p->ad.1cmp_ad_1i fe));

W tym przypadku w celu obejścia opisanego problemu funkcja naddr_ntoa jest używana jako kod opakowujący dla funkcji inet_ntoa. zapewniając przechowanie jej wyniku w liście cyklicznej o czterech różnych buforach tymczasowych11. char * naddr ntoatnaddr a)

{

Idefine NUM_BUF$ A static irtt butno; static struct { char

str[16];

/* xxx.xxx.xxx.xxx\0 */

) bufs[NUM_BUF$]; char *s; struct 1n_addr addr: addr,s_addr - a; s - strcpy(bufs[bufno].str, inet_ntoa(addr)); butno - (bufno+1) % NUM_BUFS; return s:

)

3.1.6. Wskaźniki na funkcje Często przydatnym rozwiązaniem jest sparametryzowanie funkcji poprzez przekazywa­ nie jej do innej funkcji jako argumentu. Jednak język C nie dopuszcza przekazywania funkcji jako argumentów. Możliwe jest jednak przekazanie jako argumentu wskaźnika na funkcję. Na listingu 3.212 funkcja getfile, używana do przetwarzania plików w cza­ sie procesu odtwarzania po awarii, pobiera parametry fili oraz skip. Służą one do 111netbsdsrc/sbin/routed/rdisc. c: 121 - 125. 11 netbsdsrc/sbin/routed/trace.c: 123 - 139. 12netbsdsrc/sbin/restore/tape.c: 177 - 837.

Rozdział 3. ♦ Zaawansowane typy danych języka C

79

określenia, w jaki sposób należy odczytywać lub pomijać dane. Funkcja jest wywoły­ wana (listing 3.2:1) z różnymi argumentami (listing 3.2:3) w celu wykonania począt­ kowego przeglądu danych lub odtworzenia albo pominięcia plików danych. Listing 3.2.

Parametryzacja przy użyciu argumentów funkcji

/* * Verify that the tape drive can be accessed and * that it actually is a dump tape.

*/

void setup()

{

[...]

getfile(xtrmap. xtrmapskip): • --------------

[...]

- ¡ I ] Wywołanie z argumentami będącymi wskaźnikami na funkcje

) /* Prompt user to load a new dump volume void getvoldnt nextvol)

1

*/

[...] getfiletxtrlnkfile. xtrlnkskip): • ----------

[...] getfileixtrfile. xtrskip): • ----------------

[...]

-n i

-ni

} * skip over a file on the tape

*/ void sklpfileO

{

curfiie action - SKIP. getfileixtrnull. xtmull). • ------------------------

-fU

) /* Extract a file from the tape. */ void getfileivoid (*flll)(char *. long). (*sk1p)(char *. long))

(

[...] (*fill) TP BSIZE ? • -----fssize : (curblk - 1) * TP BSIZE + size)):

-[ 2 ] Wywołanie funkcji przekazanej w argumencie

EW1 (*skip)(clearedbuf. (longKslze > TP BSI2E ? • ------TP BSIZE size)):

-

12]

[...] C*fi 11 Jtichar *)buf. ClongX(curblk * TP BSIZE) + size)): •-

-[21

[...] 1 /* Write out the next block of a file. */ static void xtrflleichar *buf. long size)

1

)

/* Skip over a hole in a file. */ static void xtrskipichar *buf. long size)

(

)

/* Collect the next block of a symbolic link */ static void

- [ } ] Funkcje przekazywane jako parametry argumentów

80

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source xtrlnkf11e(char *buf, long size)

( [...] )

/* Skip over a hole in a symbolic link (should never happen) static void xtrlnksklp(char *buf. long size)

I

*1

)

/* Collect the next block of a bit map. */ static void xtrmap(char *buf. long size)

{ [...I }

/* Skip over a hole in a bit map (should never happen) static void xtrmapsk1p(char *buf. long size)

*/

{[••])

/* Noop. when an extraction function is not needed. */ void xtrnulKchar *buf. long size)

{ return: ) Wiele plików z bibliotek języka C, takich jak qsort i bsearch, pobiera argumenty będące wskaźnikami na funkcje, które określają sposób ich działania13. getnfileO

(

[...] qsorttnl. nname. sizeof(nltype). valcmp):

[...] 1 valcmp(n1type *pl. nltype *p2)

1

if ( pl -> value < p2 -> value ) ( return LESSTHAN:

)

1f ( pl -> value > p2 -> value ) ( return GREATERTHAN.

)

return EQUALTO;

} W powyższym przykładzie sposób, w jaki funkcja qsort porównuje elementy sortowanej tablicy, określa funkcja valcmp. Wreszcie, wskaźniki na funkcje mogą być używane do parametryzowania sterowania w treści kodu. W poniższym przykładzie wskaźnik closefunc jest używany do przecho­ wywania funkcji, która zostanie wywołana w celu zamknięcia strumienia fin w zależno­ ści od sposobu jego otwarcia14. void retrlevetchar *cmd. char *name)

{

int (*closefunc)(FILE *) - NULL:

[...] If (cmd “ 0) { fln - fopen(name. "r"). closefunc * fclose:

C..J 1f (cmd) (

13netbsdsrc/usr.bin/gprof/gprofc: 216 - 536. 14nelbsdsrc/lihexec/ftpci/flpdc: 792 —860.

Rozdział 3. ♦ Zaawansowane typy danych języka C

81

[...] f1n - ftpdj>open(lIne. "r". 1). closefunc • ftpd_pclose:

[...] (*closefunc)(fin):

}

3.1.7. Wskaźniki jako aliasy Wskaźniki są często używane do tworzenia aliasów pewnych wartości1'. struct output output - (NULL, 0. NULI. OUTBUFSIZ. 1.0):

C...3 struct output *outl - Soutput:

W powyższym przykładzie kodu poddany dereferencji wskaźnik outl może zostać użyty zamiast oryginalnej wartości zmiennej output. Aliasów używa się z wielu różnych powodów.

Kwestie w ydajnościow e Przypisanie wskaźnika jest bardziej wydajne od przypisania większego obiektu. W po­ niższym przykładzie wskaźnik curt mógłby być zmienną strukturalną, a nie wskaźnikiem na taką strukturę. Jednak odpowiednia instrukcja przypisania byłaby mniej wydajna, gdyż wiązałaby się ze skopiowaniem zawartości całej struktury1'’. static struct termos cbreakt. rawt. *curt:

[...] curt - useraw ? &rawt : ¿cbreakt;

Odwołania do staty czn ie inicjalizowanych danych Zmienna jest używana do wskazywania na różne wartości danych statycznych. Najczęściej spotykany przykład w tym przypadku to zbiór wskaźników znakowych wskazujących na różne ciągi znaków1 . char *s;

[...] s - *(opt->bval) ? "True“ : "False";

Im plem entacja sem antyki odwołań do zmiennych w k o n tek ście globalnym Mało przyjazny tytuł niniejszego punktu odnosi się do odpowiednika użycia wskaźni­ ków wywołań przez referencję, tyle że z wykorzystaniem zmiennej globalnej zamiast argumentu funkcji. Zatem globalna zmienna wskaźnikowa jest używana do odwoływa­ nia się do danych, do których należy uzyskać dostęp i zmodyfikować je w innym miej­ scu. W przedstawionym poniżej generatorze liczb pseudolosowych wskaźnik fptr jest

15netbsdsrc/bin/sh/output. c: 81-84. 16netbsdsrc/lib/libcurses/tty. c: 66 - 171. 17netbsdsrc/games/rogue/room.c: 629 - 635.

Czytanie Kodu. Punkt widzenia twórców oprogramowania open-source

82

inicjalizowany tak, aby wskazywał na wpis w tabeli ziaren generatora liczb pseudolosowych. W podobny sposób jest on ustawiany w funkcji srrandomO. W końcu, w funkcji rrandom() zmienna fptr jest używana do zmodyfikowania wartości, na którą wskazuje18. static long rntb[32] - ( 3. 0x9a319039. 0x32d9c024. 0x9b663182. 0x5dalf342.

[...] }: static long *fptr - &rntb[4];

[...] void srrandomdnt x)

{

[..,] fptr - &state[rand_sep];

[ ..] ) long rrandomt)

{ *fptr +» *rptr: 1 - (*fptr » 1) 8 0x7fffffff.

Równoważny rezultat otrzymalibyśmy, przekazując fp tr jako argument do funkcji srrandonK) oraz rrandom(). Podobne podejście jest często wykorzystywane w celu uzyskiwania dostępu do mody­ fikowalnej kopii danych globalnych10. WINDOW scr_buf; WINDOW *curscr * &scr_buf;

[..] movetshort row. short col):

I

curscr->_cury - row; curscr->_curx - col. screen dirty - 1.

)

3.1.8. Wskaźniki a ciągi znaków W języku C literały znakowe są reprezentowane przez tablicę znaków zakończoną zna­ kiem zerowym ' \ 0 '. W rezultacie ciągi znaków są reprezentowane przez wskaźnik na pierwszy znak sekwencji zakończonej znakiem zerowym. W poniższym fragmencie kodu można zobaczyć, w jaki sposób kod funkcji bibliotecznej języka C strlen prze­ suwa wskaźnik wzdłuż ciągu znaków przekazanego jako parametr funkcji do momentu osiągnięcia jego końca. Następnie odejmuje wskaźnik na początek ciągu od wskaźni­ ka na końcowy znak zerowy w celu obliczenia i zwrócenia długości ciągu2'1. 18 19 20

netbsdsrc/games/rogue/random.c\ 62 - 109. netbsdsrc/games/rogue/curses.c'. 121 - 157. t 1 nelbsdirc/lib/Iibc/string/sirlen.c: 51 - 59.

Rozdział 3. ♦ Zaawansowane typy danych języka C

83

slze_t

strlentconst char *s tr)

(

register const char *s; fo r (s - Str; *s; r+S) returnCs - str):

}

A Czytając kod operujący na ciągach znaków, należy pamiętać o ¡śmiejącym rozróżnie­ niu na wskaźniki znakowe i tablice znaków. Choć obie konstrukcje są często używa­ ne do reprezentowania ciągów znaków (ze względu na fakt. że tablica znaków jest automatycznie konwertowana na wskaźnik na pierwszy znak w tablicy w momencie przekazania do funkcji), odpowiednie typy i operacje, które można na nich wykonywać, są różne. Przykładowo, poniższy kod definiuje zmienną pw_fi1e jako wskaźnik na znak wskazujący na stałą znakową zawierającą sekwencję "/etc/passwd"21. static char *pw_f11e - "/etc/passwd",

Rozmiar zmiennej pw_file na maszynie Autora wynosi 4 i może ona zostać zmodyfi­ kowana tak, aby wskazywała inne miejsce, jednak próba zmiany zawartości pamięci, na którą wskazuje, spowoduje niezdefiniowane zachowanie. Z drugiej strony, poniższy wiersz definiuje zmienną line jako tablicę znaków zainicja­ lizowaną treścią "/dev/XtyXX", po której występuje wartość zerowa22. static char I1ne[] - "/dev/XtyXX":

Zastosowanie operatora sizeof względem ine daje w wyniku 11. Zmienna 1ine zawsze będzie odwoływać się do tego samego obszaru pamięci, a elementy które zawiera mogą być dowolnie modyfikowane23. Hne[5] - 'p':

Pamiętanie o różnicach istniejących między tablicami znaków a wskaźnikami podczas czytania kodu jest istotne, gdyż obie konstrukcje są używane, często zamiennie, w po­ dobnych celach. W szczególności żaden kompilator języka C nie ostrzega użytkownika o sytuacji, w której obie zostaną przypadkowo wymieszane w ramach różnych plików. Weźmy pod uwagę poniższe dwie (niezwiązane ze sobą) definicje oraz odpowiednią deklarację24,25,26. char version[] - "332": char ‘ version - "2.1.2": extern char ‘ version:

21netbsdsrc/distrib/utils/libhack/getpwent.c: 45. "2netbsdsrc/lib/libutil/pty.c: 71. 23netbsdsrc/lib/libutil/pty.c. 81. 24netbsdsrc/usr.bin/less/less/version.c: 575. 25netbsdsrc/libexec/identd/version.c: 3. 26netbsdsrc/Iibexec/identd/identd.c: 77.

84

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

A

Obie definicje są używane do zdefiniowania ciągu informującego o wersji, który jest zapewne przekazywany do funkcji printf w celu wydrukowania na ekranie. Jednakże deklaracja extern char *version może być użyta jedynie w celu uzyskania dostępu do zmiennej zdefiniowanej jako char *version. Choć konsolidacja pliku źródłowego z plikiem zawierającym definicję char version[] zwykle nie spowoduje wygenerowa­ nia błędu, wynikowy program zakończy awaryjnie działanie po uruchomieniu. Zmienna version zamiast wskazywać na obszar pamięci, zawierający ciąg z informacją o wersji, będzie prawdopodobnie wskazywała na obszar pamięci, której adres jest reprezento­ wany przez wzorzec bitowy ciągu znaków "332" (0x33333200 w przypadku architektur niektórych procesorów).

3.1.9. Bezpośredni dostęp do pamięci f~i~| Ze względu na fakt, że wskaźniki są wewnętrznie reprezentowane jako adresy pamię­ ci, można spotkać kod niskopoziomowy wykorzystujący wskaźniki do uzyskiwania dostępu do obszarów pamięci związanych z zasobami sprzętowymi. Wiele urządzeń peryferyjnych, takich jak karty graficzne i sieciowe, wykorzystuje ogólnodostępną pamięć w celu wymiany danych z procesorem systemowym. Zmienna zainicjalizowana tak, aby wskazywała na ten obszar, może być z łatwością wykorzystywana do komu­ nikowania się z danym urządzeniem. W poniższym przykładzie zmienna video jest ustawiana tak, aby wskazywała na obszar pamięci zajmowany przez sterownik ekranu (0xe08b8000). Zapis do tego obszaru przy przesunięciu określonym przez dany wiersz i kolumnę (vid^ypos oraz vid_xpos) powoduje pojawienie się na ekranie odpowiednie­ go znaku (c)27. static void v1d_wrchar(char c)

ł volatile unsigned short ‘video; video « (unsigned short *)(0xe08b8000) ♦ vid_ypos * 80 ♦ vid_xpos: •video - (*video 8 OxffOO) | OxOfOO | (unsigned shortlc:

} Należy pamiętać, że współczesne systemy operacyjne zapobiegają uzyskiwaniu dostępu do zasobów sprzętowych przez programy użytkownika bez podjęcia wcześniej odpo­ wiednich kroków. Tego rodzaju kod spotyka się w zasadzie tylko w przypadku badania systemów osadzonych lub kodu jądra systemu i sterowników urządzeń. Ćwiczenie 3.1. Zaproponuj własną implementację stosu opartego na wskaźnikach z bi­ blioteki compress2S używając indeksu tablicy. Zmierz różnicę w szybkości i skomentuj czytelność obu implementacji. Ćwiczenie 3.2. Zlokalizuj na płycie CD-ROM dołączonej do książki trzy wystąpienia kodu używającego wskaźników z każdego z wymienionych powodów.

27 28

netbsdsrc/sys/arch/pica/pica/machdep.c: 951 - 958. netbsdsrc/usr. bin/compress/zopen. c

Rozdział 3. ♦ Zaawansowane typy danych języka C

85

Ćwiczenie 3.3. Jeżeli Czytelnik zna język C++ lub Java, może spróbować wyjaśnić, w jaki sposób można zminimalizować (w języku C++) lub uniknąć (w języku Java) używania wskaźników. Ćwiczenie 3.4. Czym różni się wskaźnik języka C od adresu pamięci? Jaki ma to wpływ na zrozumienie kodu? Jakie narzędzia wykorzystują istniejącą różnicę? Ćwiczenie 3.5. Czy ciągi znaków zawierające informacje o wersji programu powinny być reprezentowane jako tablice znaków czy jako wskaźniki na ciągi znaków? Uza­ sadnij odpowiedź.

3.2. Struktury Konstrukcja języka C struct to zgrupowanie elementów danych, które umożliwia [Tl używanie ich wspólnie. Struktury są używane w programach pisanych w C w celu: ♦ zgrupowania elementów danych używanych zwykle wspólnie; ♦ zwrócenia wielu elementów danych z funkcji; ♦ konstruowania powiązanych struktur danych (podrozdział 4.7); ♦ odwzorowania organizacji danych w urządzeniach sprzętowych, łączach sieciowych oraz nośnikach danych; ♦ implementacji abstrakcyjnych typów danych (podrozdział 9.3.5); ♦ tworzenia programów zgodnie z paradygmatem obiektowym. Poniższe punkty stanowią rozwinięcie dyskusji na temat użycia struktur i poruszają zagadnienia nie omawiane w innych podrozdziałach.

3.2.1. Grupowanie elementów danych Struktury są często używane w celu tworzenia grup powiązanych elementów, które zazwyczaj są używane wspólnie jako całość. Sztandarowym przykładem jest tu repre­ zentacja współrzędnych pozycji na ekranie'1. struct point ( in t col. line:

): W innych przypadkach może chodzić o liczby zespolone lub pola tworzące wiersze tabeli.

3.2.2. Zwracanie wielu elementów danych z funkcji |T| Kiedy wynik działania funkcji należy wyrazić używając więcej niż jednego podstawo­ wego typu danych, wiele elementów wyniku można zwrócić albo poprzez argumenty wywołania funkcji przekazane przez referencję (patrz podrozdział 3 .1.3), albo grupując 29

netbsdsrc/games/snake/snake/xnake.c: IS -1 1 .

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

86

je w ramach zwracanej struktury. W poniższym przykładzie funkcja difftv zwraca róż­ nicę między dwiema wartościami czasowymi wyrażonymi w sekundach (tv sec) oraz w mikrosekundach (tv_usec) w postaci struktury timeval30. static struct timeval d1fftv(struct timeval a. struct timeval b)

{

static struct timeval diff; dlff.tv_sec - b.tv_sec - a.tv_sec: if ((diff,tv_usec - b.tv_usec - a tv_usec) < 0) { diff tv_sec--: diff.tv usee +- 1000000;

}

return(dlff);

}

3.2.3. Odwzorowanie organizacji danych [71 Kiedy dane są przesyłane przez sieć. przenoszone z i na urządzenie pamięci masowej lub kiedy programy bezpośrednio komunikują się ze sprzętem, struktury są często uży­ wane w celu reprezentowania sposobu organizacji danych w przypadku takiego medium. Poniższa struktura reprezentuje blok poleceń karty sieciowej Intel EtherExpress31. struct fxp_cb nop { void *filT[2]: volatile ujntl6_t cb_status; volatile u_intl6_t cb_command; volatile u 1ntl6 t link_addr;

): m

Kwalifikator vol at i1e jest używany w celu oznaczenia, że odpowiednie pola pamięci są używane przez obiekty pozostające poza kontrolą programu (w tym przypadku chodzi o kartę sieciową). Kompilator nie może więc przeprowadzać optymalizacji na takich polach, na przykład usuwać nadmiarowych referencji.

[71 W określonych przypadkach można zadeklarować pole bitowe (ang. bit field), określa­ jące ścisły zakres bitów używanych do przechowywania określonej wartości w danym urządzeniu32. struct fxp_cb_config { volatile u_int8 t

byte_count:6.

" :2:~ volatile u_int8_t rx_fifo_limit,4, txjfifo limit:3.

:l7

W powyższym przykładzie liczba przesyłanych bajtów określona jako byte_count ma zajmować 6 bitów, natomiast ograniczenia kolejek FIFO odbiornika i nadajnika mają zajmować, odpowiednio, 4 i 3 bity w urządzeniu sprzętowym. 10netbsdsrc/usr. bin/named/dig/dig.c: 1221 - 1233. 31 netbsdsrc/sys/dev/pci/ifJxpreg.lv 102 - 107. 32

netbsdsrc/sys/dev/pci/ifjxpreg.h: 116 - 125.

Rozdział 3. ♦ Zaawansowane typy danych języka C

87

Pakiety danych sieciowych również są często kodowane przy użyciu struktur języka C w celu określenia struktury ich elementów, co ukazuje poniższa klasyczna definicja nagłówka pakietów TCP33. struct tcphdr ( u_lntl6_t th_sport. u_lntl6_t th_dport. tcp_seq th_seq: tcp_seq th_ack:

/* /* /* /*

source port */ destination port */ sequence number */ acknowledgement number */

Wreszcie, struktury są również używane w celu odwzorowania sposobu przechowy­ wania danych na nośnikach danych, na przykład dyskach lub taśmach. Przykładowo, właściwości dysku partycji systemu MS-DOS określa się za pomocą tak zwanego bloku parametrów BIOS. Jego pola odwzorowuje poniższa struktura34. struct bpb33 ( u_intl6_t u_1nt8_t u_intl6_t u_int8_t u_intl6_t u_1ntl6_t u_int8_t u_intl6_t u_int!6_t u int!6 t u_intl6_t

bpbBytesPerSec: bpbSecPerClust; bpbResSectors; bpbFATs; bpbRootDirEnts: bpbSectors. bpbMedia: bpbFATsecs: bpbSecPerTrack; bpbHeads: bpbHiddenSecs:

/* /* /* /* /*

/* /* /* /*

/* /*

bytes per sector */ sectors per cluster */ number of reserved sectors */ number of FATs */ number of root directory entries *1 total number of sectors */ media descriptor */ number of sectors per FAT */ sectors per track */ number of heads */ number of hidden sectors */

A Sposób uporządkowania pól w ramach struktury jest zależny od architektury oraz kom­ pilatora. Ponadto reprezentacja różnych elementów w strukturze jest zależna od archi­ tektury oraz systemu operacyjnego (system operacyjny może wymuszać na procesorze dostosowanie się do określonej kolejności przechowywania bajtów). Nawet proste typy danych, takie jak liczby całkowite, mogą posiadać swoją wartość przechowywaną na różne sposoby. Stąd użycie struktur w celu odwzorowania zewnętrznych danych sta­ nowi z gruntu nieprzenośne rozwiązanie.

3.2.4. Programowanie obiektowe |~i~1 W języku C struktury są czasem używane do tworzenia konstrukcji przypominających obiekty poprzez zgrupowanie razem elementów danych i wskaźników na funkcje w celu zasymulowania pól i metod klasy. W poniższym przykładzie struktura domain, reprezen­ tująca różne domeny protokołu sieciowego (na przykład internet, SNA, IPX), grupuje dane dotyczące określonej domeny, takie jak jej rodzina dom_family oraz metody służą­ ce do operowania na nich, takie jak metoda inicjalizacji tabeli routingu dom_rtattach35. struct domaln { 1nt dom_fam11y: char *dom name:

33nelbsdsrc/sys/netinet/lcp.h: 43 - 47. u netbsdsrc/sys/msdosfs/bpb.h: 22 - 34. 35netbsdsrc/sys/sys/dornain.h: 50 - 65.

/* AF_xxx */

88

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source void

(*dom 1nlt)(void).

/* initialize domain data structures */

[...] int int 1nt

(*dom_rtattach)(void **. int): /* initialize routing table *> dom_rtoffset: /* an arg to rtattach. in bits */ domjnaxrtkey; /* for routing layer */

I: Po wprowadzeniu takiej deklaracji, zmienne określonego typu strukturalnego, czy też ściślej rzecz ujmując — wskaźniki na typ strukturalny — po odpowiednim zainicjali­ zowaniu mogą być traktowane w sposób przypominający użycie obiektów w językach C++ i Java36. for (dom - domains, dom. dom - dom->dom_next) if (dom->dom_family — i && dom->dom_rtattach) { dom->dom_rtattach((void **)&nep->ne_rtab1e[1]. dom->dom_rtoffset): break;

Ze względu na fakt, że „obiekty” mogą być inicjalizowane za pomocą różnych „metod” (wskaźników na funkcje), a jednak są używane poprzez ten sam interfejs (wywołania następują poprzez te same nazwy składowych struktur), opisana technika stanowi im­ plementację metod wirtualnych (ang. virtual methods), dostępnych w językach obiekto­ wych, oraz programowania polimorficznego (ang. polymorphic programming). W rze­ czywistości, ze względu na fakt, że obiekty należące do tej samej klasy współużytkują swoje metody (ale nie pola), wskaźniki na metody są często dzielone przez różne obiekty dzięki przechowywaniu w strukturze „obiektów” jedynie wskaźnika na inną strukturę zawierającą wskaźniki na faktyczne metody37. struct file {

[...] short f_type: /* descriptor type */ short f_count: /* reference count */ short fjnsgcount; /* references from message queue */ struct ucred *f_cred: /* credentials associated with descriptor */ fileops ( struct Int (*fo_read)(struct file *fp, struct uio *u1o. struct ucred *cred); 1nt (*fo_wnte)(struct file *fp. struct uio *uio. struct ucred *cred); 1nt (*fo_ioctl Kstruct file *fp. ujong com. caddr_t data, struct proc *p): int (*fo_pollKstruct file *fp. int events. struct proc *p): 1nt (*fo_close)(struct file *fp. struct proc *p); ) *f_ops off_t f_offset. caddr_t f_data: /* vnode or socket */

): W powyższym przykładzie każdy obiekt reprezentujący otwarty plik lub gniazdo dzieli swoje metody read, write, ioctl, poi 1 oraz close poprzez składową struktury f_ops, wskazującą na dzieloną strukturę fi 1eops.

36 netbsdsrc/sys/kem/vfs jiubr. c : 1 4 3 6 - 1440. 37 netbsdsrc/sys/sys/file.h: 51 - 73.

Rozdział 3. ♦ Zaawansowane typy danych języka C

89

Ćwiczenie 3.6. Alternatywnym sposobem przekazywania do funkcji danych typu tabli­ cowego przez wartość i zwracania ich jako rzeczywistych wartości jest zawarcie tablicy w strukturze. Zlokalizuj takie przykłady na płycie dołączonej do książki. Wyjaśnij, dlaczego takie podejście nie jest wykorzystywane zbyt często. Ćwiczenie 3.7. Zlokalizuj 20 oddzielnych wystąpień struktur na płycie dołączonej do książki i określ powód ich wykorzystania. Zapoznaj się z biblioteką struktur danych ogólnego przeznaczenia, taką jak STL języka C++ lub ja v a .u til. Wskaż, które wystą­ pienia można by zapisać korzystając z biblioteki. Zminimalizuj czas poświęcony każ­ demu wystąpieniu. Ćwiczenie 3.8. Wiele technik, bibliotek oraz narzędzi obsługuje przenośne kodowanie danych w celu ich przenoszenia między aplikacjami. Określ techniki mające zastoso­ wanie w Twoim środowisku i porównaj je z użyciem podejścia bazującego na struktu­ rach języka C.

3.3. Unie Konstrukcja języka C union grupuje elementy, które dzielą ten sam obszar pamięci. [T| Możliwy jest dostęp tylko do jednego elementu naraz spośród współużytkujących taki obszar. Unie są używane w języku C w celu: ♦ zapewnienia wydajnego wykorzystania pamięci; ♦ zaimplementowania polimorfizmu; ♦ umożliwienia dostępu do danych przy użyciu różnych reprezentacji wewnętrznych.

3.3.1. Wydajne wykorzystanie pamięci Często spotykane uzasadnienie wykorzystania unii dotyczy współużytkowania tego samego obszaru pamięci w dwóch różnych celach. Ma to na celu zaoszczędzenie przy­ najmniej kilku bajtów pamięci. Istnieją przypadki, wktórychunie są wykorzystywane wyłącznie w tym celu. Jednak w przypadku, gdy urządzenia wbudowane obsługują pamięć o wielomegabajtowej pojemności, używanie unii staje się nieuzasadnione. Oprócz starszego kodu można również spotkać przypadki, gdy duża liczba obiektów opartych na unii uzasadnia dodatkowe wysiłki związane z napisaniem odpowiedniego kodu. Poniższy przykład pochodzący z funkcji malloc standardowej biblioteki języka C to typowy przypadek1*. union overhead ( union overhead *ov_next: struct ( u_char ovujnagic. u_char ovu index. #i fdef RCHECK u_short ovu_rmagic:

/* when free */ /* magic number */ /* bucket # */ /* range magic number */

18netbsdsrc/lib/libc/stdlib/malloc.h: 78 - 92.

90

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source ujong

ovu size:

/* actual block size */

#endif } OVU;

iPdeflne Idefine #define #define

ovjiaglc ovjndex ov_rmag1c ov size

ovu ovu_magic ovu.ovu_index ovu.ovu_rmagic ovu.ovu size

Bloki pamięci mogą być zajęte lub wolne. W każdym przypadku muszą być przecho­ wywane różne wartości a ze względu na fakt, że blok nie może być jednocześnie wolny i zajęty, mogą one dzielić tę samą przestrzeń pamięci. Dodatkowe koszty programistycz­ ne związane z konserwacją takiego rozwiązania są amortyzowane tysiącami elementów, które są przydzielane w wyniku wywołań funkcji bibliotecznych. fil Warto zwrócić uwagę na definicję makro, która występuje po definicji struktury ovu. Takie definicje są często używane jako skrócona forma odwoływania się bezpośrednio do składowych struktury w ramach unii bez konieczności używania na początku nazwy składowej unii. Stąd przedstawiony kod ma postać39 op->ov_index - bucket;

zamiast bardziej rozbudowanego odwołania op->ovu.ovu_index.

3.3.2. Implementacja polimorfizmu Najczęściej występującym powodem wykorzystania unii jest chęć zaimplementowania polimorfizmu. W tym przypadku ten sam obiekt (zwykle reprezentowany przez struktu|T1 rę języka C) jest używany w celu reprezentowania różnych typów. Dane dla takich różnych typów są przechowywane w oddzielnych składowych unii. Dane polimorficzne przechowywane w ramach unii pozwalają również zaoszczędzić pamięć w przypadku różnych konfiguracji. Jednak w tym przypadku unia jest używana w celu wyrażenia charakterystyki obiektu, który przechowuje różne typy danych, a nie zaoszczędzenia (T| zasobów. Unie używane w ten sposób są zwykle zawarte w strukturze zawierającej pole typu, które określa, jakiego rodzaju dane są przechowywane w unii. To pole jest często reprezentowane za pomocą typu wyliczeniowego, a jego nazwą jest zwykle type. Poniż­ szy przykład, część biblioteki RJPC (ang. Remote Procedurę Cali; zdalne wywoływanie procedur), zawiera strukturę używaną do reprezentowania komunikatów RPC. Obsłu­ giwane są dwa różne rodzaje komunikatów: wywołanie oraz odpowiedź. Typ wylicze­ niowy msg_type stanowi rozróżnienie między tymi dwoma typami, natomiast unia zawie­ ra strukturę z elementami danych dla każdego typu4*1. enum msg_type ( CALl-O. REPLY-1

}: [...] struct rpcjnsg 1 u_mt32_t enum msg_type

rm_x1d; rm_direction.

19 nelbsdsrc/lib/libc/stdlib/malloc.c: 213. 4(1netbsdsrcJinclude/rpc/rpcjnsg.h\ 5 4 - 158.

Rozdział 3. ♦ Zaawansowane typy danych języka C

91

unlon ( struct call_body RM_cmb; struct reply_body RM_rmb: ) ru:

):

3.3.3. Uzyskiwanie dostępu do różnych reprezentacji wewnętrznych Ostatnie użycie unii jest związane z przechowywaniem danych w ramach jednego pola unii oraz uzyskiwaniem dostępu do innego w celu przeniesienia danych między róż­ nymi reprezentacjami wewnętrznymi. Choć tego rodzaju rozwiązania są z gruntu nieprzenośne, można bez przeszkód przeprowadzać pewne konwersje. Inne są przydatne w pewnych określonych przypadkach, zależnych od danej maszyny. Poniższa definicja struktury jest używana przez program archiwizujący tar w celu reprezentowania infor­ macji o każdym pliku z archiwum41. unlon record { char charptr[RECORDSIZE] struct header ( char name[NAHSIZ]: char mode[8]: char uid[S]: char g1d(8]; char s1ze[l2]: char mtime[12]; char chksum[8]: char linkflag; char 1lnkname[NAMSIZ]: char maglc[8]; char unamefTUNMLEN]: char gname[TGNMLEN]; char devmajor[8]; char devminor[8]: } header:

): Aby umożliwić wykrywanie uszkodzenia danych, w polu chksum jest umieszczana suma bajtów wszystkich rekordów składających się na plik (wliczając w to rekord nagłów­ kowy). Programy wykorzystują składową unii charptr do iteracyjnego przeglądania danych nagłówka bajt po bajcie, obliczając sumę kontrolną oraz składową header w celu uzyskania dostępu do określonych pól nagłówka. Ze względu na fakt, że typy całkowite w języku C (wliczając znaki) są poprawne w zakresie wszystkich możliwych wzorców bitowych, jakie mogą reprezentować, uzyskiwanie dostępu do wewnętrznej reprezentacji innych typów języka C (wskaźników, liczb zmiennoprzecinkowych i in­ nych typów całkowitych) jako typu całkowitego jest z założenia operacją dozwoloną. Jednak operacja odwrotna — generowanie innego typu poprzez jego reprezentację cał|T| kowitą — nie zawsze daje poprawne wyniki. W przypadku większości architektur ope­ racją bezpieczną jest generowanie typów niecałkowitych na podstawie danych, które stanowią starszą wersję kopii ich wartości.

41 netbsdsrc/usr. bin/file/tar.h: 36 - 54.

92

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Poza tym wykorzystanie struktur w celu uzyskania dostępu do zależnych od architektu­ ry elementów danych w pewnym innym formacie jest działaniem z gruntu nieprzenośnym. Może to być przydatne w przypadku interpretowania typu danych w oparciu o jego reprezentację lub tworzenia typu danych na podstawie jego reprezentacji. W przy­ kładzie z listingu 3.342 unia u jest używana w celu uzyskania dostępu do wewnętrznej reprezentacji liczby zmiennoprzecinkowej v w formie mantysy, wykładnika oraz zna­ ku. Taka konwersja jest wykorzystywana do rozbicia liczby zmiennoprzecinkowej na znormalizowany ułamek oraz całkowitą potęgę liczby 2. Listing 3.3. Uzyskiwanie dostępu do wewnętrznej reprezentacji typu przez wykorzystanie unii________ double frexpidouble value, int *eptr) (

{

-Zwracany wykładnik

union { double v: • ----------struct < u int u mant2 : 32 * u.mt ujrantl : 20: u int u exp 11;4 u 1nt u sign : 1; 1 s:

- Wartość je st przechowywana >v tym polu -D ostęp do reprezentacji wewnętrznej odbywa się przez to pole -M antysa -W ykładnik

) a: if (value) { u.v - value;»--------*eptr - u.s.u_exp - 1022 u.s.u_exp - 1022:*---retum(u v);»--------) else { *eptr - 0: returnt(double)O):

1

-'Zachowanie wartości - Pobranie i ustawienie wykładnika ze znakiem - Wykładnik zerowy -Zwrócenie znormalizowanej mantysy

)

Ćwiczenie 3.9. Zlokalizuj 20 różnych wystąpień unii na płycie dołączonej do książki i sklasyfikuj powody ich wykorzystania. Zminimalizuj czas poświęcony każdemu wystą­ pieniu. Utwórz wykres ilustrujący częstotliwość używania tej konstrukcji w zależności od powodu. Ćwiczenie 3.10. Zaproponuj przenośną alternatywę względem implementacji nieprzenośnych konstrukcji używanych w przypadku struktur i unii. Omów swoją propozycję w kontekście kosztów implementacyjnych, możliwości konserwacji oraz wydajności.

3.4. Dynamiczne przydzielanie pamięci Struktura danych, której rozmiar nie jest znany w momencie pisania programu lub wzrasta w podczas pracy programu, jest przechowywana w pamięci przydzielanej dynamicznie w trakcie jego działania. Programy odwołują się do pamięci przydzielanej dynamicz­ nie dzięki użyciu wskaźników. W niniejszym podrozdziale zostaną przedstawione spo­ 4~ netbsdsrc/lib/libc/arch/i386/gen/frexp.c: 48 - 72.

Rozdział 3. ♦ Zaawansowane typy danych języka C

93

soby dynamicznego przydziału pamięci w przypadku struktur wektorowych. Jednak prezentowane fragmenty kodu są bardzo podobne lub identyczne z tymi. które służą do przechowywania innych struktur danych. Na listingu 3.4'1' przedstawiono typowy przykład sposobu dynamicznego przydzielania i używania przestrzeni danych. W tym przypadku pamięć zostaje przydzielona w celu przechowywania ciągu liczb całkowitych w formie tablicy, tak więc na początku zmien­ na RRlen zostaje zdefiniowana jako wskaźnik na te liczby (listing 3.4:1). Zmienne wskaź­ nikowe muszą zostać zainicjalizowane poprzez zdefiniowanie ich wskazania na poprawny obszar pamięci. W opisywanym przypadku program wykorzystuje funkcję biblioteczną malloc języka C w celu otrzymania z systemu adresu obszaru pamięci wystarczająco obszernego, aby przechowywać w nim liczby całkowite c. Argumentem funkcji mai loc l~i~l jest liczba bajtów, jakie mają zostać przydzielone. Wykonywane tu obliczenia są typo­ we: jest to iloczyn liczby elementów, dla których ma zostać przydzielona pamięć (c) A oraz rozmiaru każdego elementu (siz e o f(in t)). Jeżeli pamięć systemowa zostanie wyczerpana, funkcja malloc wskazuje to, zwracając wartość NULL. Poprawnie napisane programy powinny zawsze sprawdzać wystąpienie takiej sytuacji (listing 3.4:2). Od tego miejsca program może rozwikływać wskaźnik RRlen (używając notacji [] lub ope­ ratora *), tak jakby była to tablica zawierająca elementy c. Należy jednak pamiętać, że nie są to metody równoważne. Funkcja sizeof zastosowana względem zmiennej tabli­ cowej zwraca rozmiar tablicy (przykładowo 40 dla tablicy zawierającej 10 czterobajto­ wych liczb całkowitych), natomiast zastosowana względem wskaźnika zwraca jedynie wartość wymaganą do jego przechowywania w pamięci (na przykład 4 w przypadku wielu współczesnych architektur). Wreszcie, kiedy przydzielona pamięć nie jest już potrzebna, musi zostać zwolniona poprzez wywołanie funkcji free. Od tego momentu A próba rozwikłania wskaźnika będzie prowadzić do niezdefiniowanego wyniku. Niezwolnienie pamięci jest błędem, który może prowadzić do tego, że program będzie po­ wodował wycieki pamięci (ang. memory leaks), które powodują stopniowe marnowanie zasobów pamięciowych systemu. Listing 3.4. Dynamiczne przydzielanie pamięci___________________________________________ in t update msgtuchar *msg. in t *msglen. i n t V l1 s t[], i n t c)


max_pwtab_num) { /* need more space 1n table */ max_pvrtab_num *- 2; pwtab - (u1d2home_t *) xrealloc(pwtab,

s1zeof(uid2home_t) * max_pwtab_num):

Listing 3.5. Dostosowanie przydziału pamięci void remember_rup datatchar *host. struct statstime *st)

(

if (rup_data_1dx >» rup_data_max) ( •rup_data_max ♦- 16: *■

44 netbsdsrc/usr.bin/sed/misc.c: 63 - 107. 4> netbsdsrc/usr.bin/rup/rup.c: 146 - 164. 46 netbsdsrc/usr.sbin/amd/hlfsdfhomedir.c. 521 -525.

Czy indeks większy o d przydzielonego rozmiaru ? ----------- Nowy rozmiar

95

Rozdział 3. ♦ Zaawansowane typy danych języka C rup data - realloc (rup data. rup_data_max * s1zeof(struct rup_data)); if T r u p d a t a — NULL) { err (1, "realloc"):

rup datatrup data idx].host - strdup(host); •rup_data[rup_data_idx].statstlme - *st: rup_data_1dx++: • --------------------------

Przechowanie danych Nowy indeki

3.4.1. Zarządzanie wolną pamięcią Powyżej wspomniano, że niezwalnianie pamięci prowadzi do sytuacji, w których pro­ gramy powodują jej wyciekanie. Jednak często można spotkać takie programy, w któ­ rych przydziela się, ale nie zwalnia pamięci4'. Ich twórcy mogą sobie na to pozwolić, jeżeli działanie programów jest krótkie — w momencie zakończenia działania cała przydzielona programowi pamięć jest automatycznie przejmowana przez system ope­ racyjny. Podobnie jest w przypadku implementacji programu skeyinit*8. Int m a i n d n t argc. char *argv[])

{

[...] skey.val - (char *)malloc(16 + 1): [... brak wywołania free(skey.val) ] ex1t(l)

A

Program skeyinit jest używany do zmiany hasła lub dodania użytkownika w systemie uwierzytelniania Bellcore S/Key. Wykonuje on swoje działania, a potem natychmiast kończy działanie, zwalniając zajmowaną pamięć. Jednak taka nieostrożna praktyka ko­ dowania może powodować problemy, kiedy ten sam kod zostanie wykorzystany ponow­ nie w programie o znacznie dłuższym czasie działania (na przykład jako część systemu oprogramowania routera). W takim przypadku program będzie powodował wycieki pamięci, co można sprawdzić korzystając z polecenia przeglądania procesów, takiego jak ps lub top w systemach uniksowych albo menedżera zadań w systemie Windows. Warto zauważyć, że w powyższym przypadku można by po prostu użyć rozwiązania, gdzie program ustawiałby wartość skey.val tak. aby zmienna wskazywała na tablicę 0 stałym rozmiarze, której pamięć przydzielono jako zmiennej lokalnej na stosie. ma1n (int argc. char *argv[])

1 char valspace[16 + 1]:

[...] skey.val - valspace:

A Często popełnianym przez początkujących programistów piszących w językach C i C++ błędem jest przyjęcie założenia, że wszystkie wskaźniki muszą zostać zainicjalizowane tak, aby wskazywały na bloki pamięci przydzielone za pomocą funkcji malloc. Choć kod zapisany w takim stylu nie jest błędny, program wynikowy jest często trudniej czytać 1 konserwować. 4'netbsdsrc/bin/mv/mv.c : 260.

48netbsdsrc/usr.bin/skeyinit/skeyinit.c: 34 - 233.

96

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

W kilku przypadkach można spotkać programy, które zawierają mechanizm przywra­ cania pamięci (ang. garbage collector), automatycznie zwalniający nieużywaną pamięć. Taka technika jest często wykorzystywana w sytuacji, gdy przydzielone bloki pamięci jest trudno śledzić, ponieważ na przykład są one współużytkowane przez różne zmienne. W takich sytuacjach z każdym blokiem zostaje związany licznik referencji (ang. refe­ rence count). Jego wartość jest zwiększana za każdym razem, gdy zostaje utworzone nowe odwołanie do bloku41*: req.ctx - ctx: req.event.time » time, ctx->ref_count-*-*-,

i zmniejszana za każdym razem, gdy referencja zostaje zniszczona51'. XtFree((char*)req): ctx->req - NULL: ctx->ref_count--;

Kiedy licznik referencji osiąga wartość 0, blok nie jest dłużej używany i może zostać zwolniony51. if (--ctx->ref_count — 0 88 ctx->free_when_done) XtFree((char*)ctx):

Inne, rzadziej stosowane podejście polega na użyciu konserwatywnego mechanizmu przywracania pamięci (ang. conservative garbage collector), który bada całą pamięć procesu, szukając adresów odpowiadających istniejącym, przydzielonym blokom pa­ mięci. Wszystkie bloki, które nie zostaną znalezione w toku procesu przeglądania, są później zwalniane. W końcu, niektóre wersje bibliotek języka C implementują niestandardową funkcję o nazwie alłoca. Przydziela ona blok pamięci używając takiego samego interfejsu jak ma 11 oc, jednak zamiast przydzielać blokowi pamięć na stercie (ang. heap) programu (jest to pamięć ogólnego przeznaczenia należąca do programu) przydzielają ona na stosie (ang. stack) programu (jest to obszar używany do przechowywania adresów zwrotnych funkcji oraz zmiennych lokalnych)52. int ofisa_intr_get(int phandle. struct of1sa_1ntr_desc *descp. int ndescs)

(

char *buf. *bp:

[...] buf - allocad):

A

Blok zwrócony przez funkcję alloca jest automatycznie zwalniany, kiedy następuje powrót z funkcji, w której ją przydzielono. Nie ma potrzeby wywoływania funkcji free w celu zwolnienia przydzielonego bloku. Oczywiście, adres pamięci przydzielonej przez funkcję alloca nie powinien nigdy być przekazywany do podprogramów wywołujących funkcję, w której ją przydzielono, gdyż w momencie wyjścia z funkcji adres ten staje się

40 XFree86-3.3/xcJIib/XT/Selection.c. 1 5 4 0 - 1542. 50 XFree86-3.3/xc/lib/XT'Selection.c: 744 - 746. 51 XFree86-3.3/xc/lib/XT/Selection.c : 563 - 564. 52

netbsdsrc/sys/dev/ofisa/ofisa.c: 225 - 244.

Rozdział 3. ♦ Zaawansowane typy danych języka C

97

niepoprawny. Funkcji alloca nie należy używać w pewnych środowiskach programi­ stycznych. takich jak FreeBSD, gdyż jest ona uważana za nieprzenośną i zależną od danej maszyny. W innych przypadkach, takich jak środowisko GNU, jej używanie jest zalecane, gdyż redukuje liczbę przypadkowych wycieków pamięci.

3.4.2. Struktury z dynamicznie przydzielanymi tablicami |T| Niekiedy pojedyncza przydzielona dynamicznie struktura jest używana w celu prze­ chowywania pewnych pól oraz tablicy zawierającej specyficzne dla struktury dane o zmiennej długości. Taka konstrukcja jest wykorzystywana w celu uniknięcia pośredniości wskaźników oraz narzutu pamięciowego związanego z posiadaniem przez ele­ ment struktury wskaźnika na dane o zmiennej długości. Zatem zamiast definicji podobnej do poniższej 3: typedef struct { XID

1d base:

[...] unslgned char *data: unsigned long datajen: } XRecordInterceptData:

/* in 4-byte units */

użyto by podobnej do następującej54: typedef struct { char *user: char *group: char *flags: char data[l]: } NAMES;

Tablica data — element struktury — jest używana jako miejsce na faktyczne dane. W momencie przydziału pamięci przechowującej strukturę jej rozmiar jest rozszerzany w oparciu o liczbę elementów w tablicy data. Od tego momentu element tablicowy jest używany tak. jakby zawierał przestrzeń dla tych elementów'15. if ((np - ma11oc(sizeof(NAMES) + ulen + glen + flen + 3)1 ~ NULU errd. ”Xs". np->user - &np->data[0]; (vo1d)strcpy(np->user. user):

A

Warto zauważyć, w jaki sposób w powyższym przykładzie przydzielana jest pamięć o jeden bajt większa od faktycznie potrzebnej. Rozmiar bloku pamięci jest obliczany jako suma rozmiaru struktury oraz powiązanych elementów danych: rozmiaru trzech ciągów znaków (ulen. glen, flen) i odpowiednich trzech znaków zerowych kończących ciągi. Jednak rozmiar struktury uwzględnia już jeden bajt dla powiązanych danych, który nie jest brany pod uwagę w czasie obliczania rozmiaru bloku pamięci. Zarządza­ nie pamięcią na najniższym poziomie jest działaniem ryzykownym i podatnym na błędy.

53XFree86-3.3/xc/include/extensions/record.h: 99 - 108. 54netbsdsrc/bin/ls/ls.h. 69 - 74. 55nelbsdsrc/bin/ls/ls.c: 470 - 475.

98

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Ćwiczenie 3.11. Zlokalizuj na płycie dołączonej do książki średniej wielkości program napisany w języku C, który wykorzystuje dynamiczne przydzielanie pamięci. Oceń odsetek całości kodu programu związany z zarządzaniem dynamicznie przydzielanymi strukturami. Oszacuj wartość ponownie, zakładając, że zwalnianie pamięci jest wyko­ nywane automatycznie przez mechanizm czyszczenia pamięci, tak jak ma to miejsce w przypadku programów pisanych w języku Java. Ćwiczenie 3.12. Większość współczesnych środowisk programistycznych oferuje spe­ cjalizowane biblioteki, opcje kompilacji lub inne narzędzia służące do wykrywania wycieków pamięci. Zidentyfikuj mechanizmy dostępne w Twoim środowisku i użyj ich w przypadku trzech różnych programów. Przeanalizuj otrzymane wyniki.

3.5. Deklaracje typedef Przykłady z poprzedniego podrozdziału zawierały deklaracje typedef służące do two­ rzenia nazw nowych typów danych. Deklaracja typedef dodaje nową nazwę (synonim) dla już istniejącego typu. Zatem po umieszczeniu poniższej deklaracji typedef unsigned char cc_t;

widząc zapis cc_t należy go odczytywać jako unsigned char. Programy pisane w języku C używają deklaracji typedef w celu zapewnienia obsługi abstrakcji, zwiększenia czy­ telności kodu, zapobieżenia problemom związanym z przenośnością oraz emulowania j mechanizmu deklaracji klas znanego z języków C++ oraz Java. Wspólne użycie typów przedrostkowych i przyrostkowych w deklaracjach języka C j czasem sprawia, że odczytanie deklaracji typedef staje się utrudnione5 . typedef char ut_Hne_t[UT_LINESIZE]:

Jednakże rozszyfrowanie takich deklaracji nie nastręcza większych problemów. Wy­ starczy traktować zapis typedef jako specyfikator przechowywania klasy, podobny do extern lub static, i odczytywać deklarację jako definicję zmiennej. static char ut_11ne_t[UT_L1NESIZE]:

Nazwa definiowanej zmiennej (w powyższym przypadku ut_J ine_t) jest nazwą typu. | Typem zmiennej jest typ odpowiadający tej nazwie. Kiedy deklaracja typedef jest używana jako mechanizm abstrakcyjny, nazwa abstraktu I jest definiowana jako synonim jego konkretnej implementacji. W rezultacie kod, który [ wykorzystuje zadeklarowaną nazwę, jest lepiej udokumentowany, gdyż jest on zapisany w kontekście odpowiednio nazwanej konstrukcji abstrakcyjnej, a nie przypadkowych szczegółów implementacji.

56

netbsdsrc/libexec/telnetd/defs.h:

124.

57 netbsdsrc/libexec/rpc.rusersd/rusers_proc.c: 86.

Rozdział 3. ♦ Zaawansowane typy danych języka C

99

W poniższym przykładzie DBT definiuje bazę danych tliang, strukturę zawierającą klucz lub element danych58. typedef struct ( void *data. size t size: ) DBT:

/* data */ /* data length */

Po tej deklaracji wszystkie podprogramy obsługi dostępu do bazy danych są definio­ wane jako działające na obiektach DBT (i innych zdefiniowanych za pomocą deklaracji typedef)59. int rec_get (const DB *. const DBT *. OBT *. u_int): Int_rec_iput (BTREE *. recno_t. const DBT *. ujnt): int _rec_put (const DB*dbp. DBT *. const DBT *. u_int); 1nt rec_ret (BTREE *, ERG *. recno_t. OBT *. DBT *).

Ze względu na fakt, że w przypadku języków C i C++ szczegóły sprzętowe typów danych języka zależą od odpowiedniej architektury, kompilatora oraz systemu operacyjnego, fil deklaracje typedef są często używane w celu zwiększenia przenośności programu albo poprzez utworzenie przenośnych nazw dla znanych wielkości sprzętowych, albo przez szereg deklaracji zależnych od implementacji6" typedef typedef typedef typedef typedef typedef typedef typedef

_signed unsigned short unsigned int unsigned long unsigned

char char short int long

int8_t: u_int8_t: 1nt16_t u_1ntl6_t 1nt32~t u_int32_t int64_t u_1nt64_t

fi] albo tworząc abstrakcyjne nazwy dla wielkości o znanej reprezentacji sprzętowej, przy użyciu jednej z wcześniej zadeklarowanych nazw61. typedef u_1nt32_t 1n_addr_t: typedef u_intl6_t 1n_port_t:

|T| Wreszcie deklaracje typedef są również często używane w celu emulowania znanego z języków C++ i Java mechanizmu, kiedy to deklaracja klasy wprowadza nowy typ. W programach pisanych w C często spotyka się deklarację typedef użytą w celu wpro­ wadzenia nazwy typu dla struktury (jest to najbliższa klasie konstrukcja występująca w C) identyfikowanej przez tę samą nazwę. Zatem poniższy przykład deklaruje path jako synonim dla s tru c t path62. typedef struct path path: struct path {

[...]

58netbsdsrcJinclude/db.k. 72 - 75. 59

netbsdsrc/lib/libc/db/recno/extern.lv. 47 - 50.

6(1netbsdsrc/sys/archJalpha/include/types.h'. 61 - 68. 61nerbsdsrc/sys/arch/arm32/include/endian.h: 61 - 62. 6" netbsdsrc/sbin/mount_portal/conf.c\ 62 - 63.

100

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Ćwiczenie 3.13. Zlokalizuj na płycie dołączonej do książki pięć różnych wystąpień każdego rodzaju użycia deklaracji typedef, o których była mowa. Ćwiczenie 3.14. W jaki sposób deklaracje typedef mogą negatywnie wpływać na czytelność kodu?

Dalsza lektura Jeżeli zagadnienia omówione w niniejszym rozdziale nie są znane Czytelnikowi, warto poszerzyć swoją znajomość języka C dzięki lekturze pozycji [K.R88]. Teoretyczne pod­ stawy implementacji rekurencyjnych typów danych bez jawnego użycia wskaźników przedstawiono w pozycji Hoare’ego [Hoa73]. Użycie wskaźników w języku C zostało zwięźle przedstawione w pozycji Sethiego i Stone’a [SS96], zaś wiele pułapek zwią­ zanych z ich użyciem omówiono w pozycji Koeniga [Koe88, s. 27 - 46]. W pozycji [CWZ90] zawarto analizę wskaźników i struktur. Istnieje wiele interesujących arty­ kułów poświęconych manipulowaniu strukturami danych opartymi na wskaźnikach i wykorzystywaniu właściwości wskaźników [FH82, Suz82, LH86]. Opis sposobu re­ alizacji funkcji wirtualnych w implementacjach języka C++ (jak również, zgodnie z informacjami podanymi w niniejszym rozdziale, w języku C) można znaleźć w pozy­ cji Ellis i Stroustrupa [ES90, s. 217 - 237], Algorytmy związane z dynamicznym przy­ dzielaniem pamięci omówiono w pozycji Knutha [Knu97, s. 435 - 452], zaś związane z nimi praktyczne implikacje w dwóch innych źródłach [Bru82, DDZ94], Pojęcie me­ chanizmu oczyszczania pamięci z licznikiem referencji omówiono w pozycji Christophera [Chr84], natomiast zarys implementacji konserwatywnego mechanizmu oczyszczania pamięci można znaleźć w pozycji Boehma [Boe88].

Rozdział 4.

Struktury danych języka C Programy działają poprzez stosowanie algorytmów względem danych. Wewnętrzna organizacja danych odgrywa istotną rolę w zakresie funkcjonowania algorytmów. Można spotkać elementy o tym samym typie zorganizowane jako kolekcje przy użyciu wielu odmiennych mechanizmów, z których każdy charakteryzują różne cechy prze­ chowywania i uzyskiwania dostępu do danych. Wektor (ang. vector) zaimplementowa­ ny jako tablica zapewnia swobodny dostęp do wszystkich elementów, ale zmiana jego rozmiaru w czasie działania programu może być mało wydajna. Wektor może być rów­ nież wykorzystywany do organizacji grup elementów w postaci tabeli (ang. table) lub ustawiony w dwóch wymiarach tworząc macierz (ang. matrix). Operacje wykonywane na wektorze są niekiedy ograniczone do występowania tylko na jednym końcu struktury danych. Mówimy wówczas o stosie (ang. stack). Operacje te muszą również czasem być wykonywane w kolejności elementów „pierwszy wchodzi, pierwszy wychodzi”, co nakazuje traktować wektor jako kolejkę (ang. queue). Kiedy kolejność elementów nie odgrywa znaczenia, do tworzenia tabel przeglądowych używane są mapy (ang. maps). natomiast zbiory (ang. sets) służą do tworzenia kolekcji elementów. Łączenie ze sobą elementów danych za pomocą wskaźników daje możliwość tworze­ nia wielu innych struktur, które są często spotykane. Lista jednokierunkowa (ang. lin­ ked list) z łatwością może być rozszerzana dynamicznie, jednak oferuje jedynie dostęp sekwencyjny (uwzględniając operacje na stosie i kolejce), natomiast odpowiednio zor­ ganizowane drzewo (ang. tree) może być używane w celu uzyskiwania dostępu do ele­ mentów danych w oparciu o wartość klucza i również może być z łatwością rozszerza­ ne dynamicznie, dopuszczając jednocześnie przeglądanie w uporządkowany sposób. Omówienie struktur danych języka zakończy zbadanie pewnych aplikacji i wzorców kodu dotyczących grafów (ang. graphs), które stanowią najelastyczniejszą reprezen­ tację łączonych struktur danych, jakie można spotkać. Języki takie jak Java, C++ i C# oferują jako części bibliotek języka abstrakcje służące do implementacji tych struktur danych. W języku C struktury danych są zwykle jawnie kodowane w samej treści programu, jednak ich właściwości i wykonywane na nich operacje są z reguły podobne. Celem niniejszego rozdziału jest nauczenie Czytelnika czytać jawnie definiowane operacje wykonywane na strukturach danych w kontekście odpowiednich abstrakcyjnych klas danych.

102

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

4.1. Wektory Najczęściej spotykaną strukturą danych w programach napisanych w języku C są wek­ tory (określane mianem buforów (ang. buffers) w przypadku ich użycia jako struktur tymczasowych). Wykorzystuje się je do przechowywania elementów tego samego typu i przetwarzania ich przy dostępie liniowym lub swobodnym. Wektor,' są zwykle reali­ zowane w kodzie języka C przy użyciu wewnętrznego typu tablicowego bez podejmo­ wania próby oddzielenia właściwości wektora od odpowiedniej implementacji. Przy­ kładowo, pierwszy element wektora jest zawsze dostępny w tablicy na pozycji O1. ProgramName - argv[0]:

W celu uniknięcia nieporozumień, od tego miejsca będziemy używać pojęcia tablica w kontekście tablicowej implementacji wektora, która najczęściej występuje w progra­ mach pisanych w C. Zakres stosowania wektorów jest bardzo obszerny i programiści wykazują w tym względzie ogromną kreatywność. Istnieje niewiele wzorców tworzenia kodu (innych niż występowanie ciągu znaków buf w nazwie tablicy — w kodzie źró­ dłowym systemu BSD można znaleźć ponad 50 000 takich przypadków), wiele moż­ liwości popełnienia błędów i bardzo niewiele metod pozwalających na rozwikłanie skomplikowanego kodu. Typowy sposób przetworzenia wszystkich elementów tablicy polega na wykorzystaniu pętli for2. char pbufO[ED_PAGE_SIZE]: for (1 - 0: 1 < E0_PAG£_SIZ£; 1++) pbufOCi] - 0;

[Tl Tablica licząca N elementów jest w całości przetwarzana w przypadku użycia zapisu for (i = 0: i < N; i++). Wszelkie inne zapisy powinny wzmóc czujność czytającego kod. Bezpiecznym działaniem jest wcześniejsze zakończenie wykonywania pętli przy A użyciu instrukcji conti nue lub wyjście z niej przy użyciu instrukcji break. Jednak jeśli spotka się kod, w którym indeks pętli jest zwiększany w jej treści, należy z wielką ostroż­ nością zbadać jego działanie. W celu inicjalizacji tablic oraz kopiowania ich zawartości często używa się funkcji bibliotecznych języka C memset oraz memcpy. Kod inicjalizacji tablicy przedstawiony powyżej można zapisać w sposób następujący3: statlc char buf[128]: memseUbuf. 0. slzeoffbuf)):

Funkcja memset powoduje ustawienie obszaru pamięci na wartość przekazaną jako drugi argument wywołania. Jest to wygodny sposób inicjalizowania ciągów jednobajtowych znaków określoną wartością (przykładowo znakiem spacji) lub liczb całkowitych war1 XFree86-3.3/contrib/prograni.i/viewres/viewres.c: 884. ' >ieibsdsrc/sys/arch/beboxJisaJif_ed.c\ 1184-1187. 3

netbsdsrc/bin/ehio/chio.c\ 659 - 662.

Rozdział 4. ♦ Struktury danych języka C

103

A tością 0. We wszystkich innych przypadkach, na przykład inicjalizacji znaków wielobajtowych (takich jak Unicode) lub liczb zmiennoprzecinkowych, jest to działanie ryzykowne i nieprzenośne. Nie ma absolutnie żadnej gwarancji, że powtórzenie da­ nej wartości bajtu da określoną liczbę zmiennoprzecinkową lub znak Unicode. |T| Wyrażenie sizeof(x) zawsze daje w wyniku poprawną liczbę bajtów dla przetworzenia tablicy x (ale nie wskaźnika) za pomocą funkcji memset lub memcpy. Daje to tę korzyść (w porównaniu z jawnym przekazaniem rozmiaru tablicy), że zmiany liczby elementów A tablicy lub rozmiaru jej pojedynczych elementów są automatycznie uwzględniane. Nale­ ży jednak zauważyć, że zastosowanie funkcji sizeof względem wskaźnika na dyna­ micznie przydzielony blok pamięci zwraca jedynie rozmiar samego wskaźnika, a nie rozmiar bloku. Podobnie funkcja memcpy jest często używana do kopiowania danych między tablicami4. 1nt 1nt

forceraapCMAPSZ]. tmpmap[MAPSZ]:

/* map for blocking combos */ t* map for blocking combos */

[...] memcpy!forcemap. tmpmap. slzeof(tmpmap));

A

A

Warto zauważyć, że w powyższym przykładzie bezpieczniejszym rozwiązaniem byłoby zastosowanie funkcji sizeof względem bufora wyjściowego — tablicy forcemap — gdyż skopiowanie przez funkcję memcpy większej liczby bajtów niż wynosi rozmiar obiektu docelowego może prowadzić do nieprzewidywalnych skutków. Funkcje memcpy i memset nie uwzględniają systemu typów języka — należy samodzielnie sprawdzić, czy argu­ menty źródła oraz celu odwołują się do elementów o tym samym typie. Ponadto zacho­ wanie funkcji memcpy jest niezdefiniowane w przypadku, gdy regiony źródłowy i doce­ lowy nachodzą na siebie. W takiej sytuacji należy używać funkcji memmove.

|T| Elementy tablicy są często zapisywane do pliku za pomocą funkcji bibliotecznej języka C fwrite5. if (fwrlteibuf. slzeof(char). n. fp) !- n) { message!."wrlteO failed, don't know why". Q);

Analogiczna funkcja fread jest następnie używ'ana w celu wczytania elementów z pliku do pamięci głównej6. 1f (freadtbuf. sizeof(char), n. fp) != n) ( clean_up("read() failed, don’t know why").

A Powyższe funkcje przechowują w pliku kopię wewnętrznej reprezentacji danych. Takie dane są z gruntu nieprzenośne między architekturami różnych systemów (na przykład maszyną z procesorem Intel a komputerem Power PC Macintosh). Obie funkcje zwra­ cają liczbę odczytanych lub zapisanych elementów. Programy powinny sprawdzać, czy funkcja wykonała działania zgodnie z oczekiwaniami, testując zwracaną wartość.

4 netbsdsrc/games/gomoku/pickmove.c: 68 - 324. 5 netbsdsrc/games/rogue/save.c: 382 - 383. 6 netbsdsrc/games/rogue/save.c: 370 - 371.

104

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Najczęściej spotykany problem w przypadku badania programów związanych z prze-. twarzaniem tablic to kod. który może uzyskiwać dostęp do elementu znajdującego się poza zakresem tablicy. Niektóre języki, takie jak Java, C # i Ada, obsługują taki błąd wywołując wyjątek. Inne języki, takie jak C i C++, zwracają wartość niezdefiniowaną lub zapisują ją pod losowym adresem pamięci (może być to adres, który przechowuje wartość innej zmiennej). W jeszcze innych, jak Perl i Tcl/Tk. następuje rozszerzenie zakresu. We wszystkich przypadkach trzeba zapewnić, aby podobne błędy nie wystę­ powały. W przypadku prostych pętli często można przedstawić nieformalny dowód, że dostęp następuje tylko do poprawnych wartości tablicy. W bardziej skomplikowanych sytuacjach można skorzystać z pojęcia niezmiennika pętli bazującego na indeksie tablicy w celu skonstruowania bardziej formalnej argumentacji dowodzącej poprawności pro­ gramu. Przykład wykorzystania takiego podejścia zaprezentowano w podrozdziale 2.11.

A

Problem ten często pojawia się w przypadku wywołań funkcji, które jako argument pobierają tablicę. Rozmiar tablicy jest często przekazywany do funkcji poprzez mecha­ nizm zewnętrzny — często dodatkowy argument, jak ma to na przykład miejsce w przypadku funkcji bibliotecznych języka C qsort oraz memcpy. W pewnych niesławnych ' przypadkach dotyczących takich funkcji jak gets i sprintf, wywoływana funkcja nie ma możliwości określenia rozmiaru tablicy. W takiej sytuacji może ona dokonać zapisu poza obszarem tablicy i w zasadzie programista nie ma możliwości zapobieżenia temu. j Weźmy pod uwagę poniższy kod7. ma1n() char

p[80];

[...) if (gets(p) - NULL || ptOJ break:

A

'NO')

Jeżeli program otrzyma na wejściu ponad 80 znaków, nadpiszą one inne zmienne lub I nawet informacje sterujące przechowywane na stosie. Tego rodzaju problem, określany I mianem przepełnienia bufora (ang. buffer overflow), jest wykorzystyw any przez twór-1 ców robaków i wirusów w celu dokonywania drobnych zmian w programie i uzyski- ■ wania dzięki temu uprzywilejowanego dostępu do zasobów systemu. Z tego powodu I funkcje biblioteczne języka C, które mogą nadpisywać bufor przekazywany jako argu- I ment, na przykład strcat, strcpy, sprintf, vsprintf, gets i scanf, powinny być używane I (o ile w ogóle) tylko w warunkach, gdy ilość zapisywanych danych jest kontrolowana I i weryfikowana przez kod wywołujący. W przypadku większości funkcji standardowe I biblioteki języka C lub rozszerzenia związane z systemem operacyjnym zapewniają ■ bezpieczne alternatywy, takie jak strncat, strncpy, snprintf, vsnpnntf oraz fgets. H W przypadku funkcji fgets kontrola kopiowanych danych jest bardzo trudna, gdyż I przetwarzane są dane wejściowe programu. Poniższy fragment kodu reprezentuje słusz- I ne podejście programisty ostrzegającego w komentarzu o użyciu funkcji, która zapisuje I do bufora bez sprawdzania jego rozmiaru*. /* Yes. we use gets not fgets. extern char *gets():

Sue me. */

7 XFree86-3.3/xc/conflg/util/nikshadow/wildmat.c: 138- 157. s XFree86-3.3/xc/config/util/mk.ihadow/wildmat.c: 134- 135.

Rozdział 4. ♦ Struktury danych języka C

105

Należy zauważyć, że chociaż uzyskiwanie dostępu do elementów leżących poza tabli­ cą jest niedozwolone, standard ANSI C i C-h- dopuszcza obliczanie adresu elementu A znajdującego się tuż za końcem tablicy. Obliczanie adresu dowolnego innego elementu znajdującego się poza tablicą jest niedozwolone i może prowadzić do niezdefiniowa­ nego zachowania w przypadku niektórych architektur, nawet jeśli nie ma miejsca uzy[T] skiwanie faktycznego dostępu do danych. Adres elementu będącego końcem tablicy jest używany jako znacznik końca zakresu, umożliwiając iteracyjne zbadanie tego zakresu9. Idefine MAX_ADS 5 struct dr {

/* accumulated advertisements */

[...] n_long dr_pref; ) *cur_drp, drs[HAX_ADSj.

/* preference adjusted By metric */

[...] ' struct dr *drp:

E...] for (drp - drs; drp < &drs[HAX__ADS]: drp++) ( drp->dr_recv_pref - 0: drp->dr_life - 0;

1 W powyższym przykładzie, choć niedozwolone jest uzyskanie dostępu do elementu drs[MAX_ADS], to jego adres jest poprawnie używany do reprezentowania pierwszego elementu poza zakresem tablicy. Zakresy są zwykle reprezentowane przy użyciu swoje[T| go pierwszego elementu oraz pierwszego elementu znajdującego się poza nimi. Rów­ noważne matematyczne sformułowanie powyższej konwencji określa, że zakresy są reprezentowane z domknięciem ich pierwszego elementu oraz z otwarciem elementu ostatniego. Ten idiom, związany z reprezentowaniem zakresów, pomaga programistom uniknąć błędów zliczenia o jeden (ang. ojf-by-one). Czytając kod wykorzystujący takie kresy asymetryczne (ang. asymmetric bounds) można z łatwością określać zakresy, które reprezentują. ♦ Liczba elementów należących do zakresu asymetrycznego jest równa różnicy między kresem górnym a dolnym. ♦ Kiedy kres górny zakresu asymetrycznego jest równy kresowi dolnemu, zakres jest pusty. ♦ Kres dolny zakresu asymetrycznego reprezentuje pierwszy zajęty element; kres górny — pierwszy wolny element. Ćwiczenie 4.1. Wybierz duży system programistyczny, zlokalizuj miejsca, w których są używane tablice i określ kategorie ich użycia. Opisz trzy wystąpienia, w przypadku których byłyby odpowiednie alternatywne, bardziej rozbudowane struktury danych (takie jak klasa vector standardowej biblioteki szablonów (STL) języka C++). Omów zalety i wady użycia abstrakcyjnego typu danych vector zamiast bezpośredniego uży­ cia wewnętrznego typu tablicowego. Spróbuj zlokalizować fragmenty kodu. których nie dałoby się zamienić na wersję wykorzystującą interfejs klasy vector. Ćwiczenie 4.2. Tablice o stałym rozmiarze nakładają różne ograniczenia na działania pro­ gramu. Zlokalizuj 10 wystąpień tego rodzaju ograniczeń w ¡śmiejącym kodzie i sprawdź, czy zostały one ujęte w dokumentacji odpowiedniego systemu. ę

netbsdsrc/sbin/routed/rdisc.c: 8 4 - 120.

106

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Ćwiczenie 4.3. Opracuj wyrażenie, które daje wartość prawdy w przypadku, gdy argu­ menty przekazywane do funkcji memcpy reprezentują niezachodzące na siebie zakresy pamięci. Przyjmij, że adresy pamięci są kodowane w sposób liniowy, ale pamiętaj, że założenie to jest fałszywe w przypadku architektur wielu procesorów. Wyjaśnij, w jaki sposób takie wyrażenie mogłoby zwiększyć niezawodność systemu, gdyby użyto go jako asercji. Ćwiczenie 4.4. Zlokalizuj przypadki, w których wywoływane są funkcje podatne na problem przepełnienia bufora i albo udowodnij, że w danym przypadku problem ten nie może wystąpić, albo przedstaw przykład sytuacji, w której może wystąpić przepeł­ nienie bufora.

4.2. Macierze i tabele Dwuwymiarowe struktury danych są zwykle określane mianem tabel (ang. table) w kon­ tekście przetwarzania danych oraz macierzami (ang. matrix) w kontekście matematycz­ nym. Te dwie struktury odróżnia również fakt. że wszystkie elementy macierzy są tego samego typu, zaś elementy tabeli są w większości przypadków różnych typów. Ta róż­ nica określa sposób użycia elementów języka C do przechowywania każdej struktury. Tabele są przechowywane i przetwarzane jako tablice struktur struct języka C. Tabele są blisko związane z relacyjnymi bazami danych. Każdy wiersz tabeli (element tablicy) reprezentuje rekord (ang. record), natomiast każda kolumna tabeli (składowa struktury) reprezentuje pole (ang. field). Tabele przechowywane w pamięci mogą być alokowane statycznie, tak jak ma to miejsce w przypadku tabel przeglądowych, o których będzie mowa w podrozdziale 4.5, lub w poniższym przykładzie, który konwertuje kody nu­ meryczne internetowego protokołu komunikatów sterujących (ICMP) na reprezenta­ cje w formie ciągów znaków10. static struct tok icmp2str[] - { { ICMPJCHOREPLY. { ICMP_SOURCEGUENCH. { ICMP_ECH0.

"echo reply" ).

"source quench" }. "echo request" }.

[...] " { 0.

NULL )

1: Tabele mogą być również alokowane dynamicznie poprzez użycie funkcji ma 11 oc, gdzie przekazuje się do niej jako argument iloczyn rozmiaru tablicy i rozmiaru struktury języ­ ka C, którą zawiera (Listing 4.1:1)’'. Listing

4.1.

Wskaźnik na strukturą jako kursor tabeli_______________________________________ struct user *usr. *usrs:

[...] if (Kusrs - (struct user *)malloc(nusers * sizeof( struct user)))) • — /"//Przydzielenie pamięci errxd. "allocate users"): dla tablicy

[...] 10netbsdsrc/usr.sbin/tcpdump/print-icmp.c: 109- 120. 11 netbsdsrc/usr.sbin/quot/quot.c: 423 - 445.

107

Rozdział 4. ♦ Struktury danych języka C

for (usr - usrs. n - nusers: ~n_>- O && usr->count: usr++) | •prlntf("X51d". tlong)SIZE(usr->space»if (count) pr1ntf(*\tX5ld", (long)usr->caunt*pnntf(“\tX-8s". usr->namefc----------

[...]

■[i] P rzeginanie labeh ■[2] Dostęp do pola

■12]

■P-]

W czasie przetwarzania tabel często zachodzi potrzeba odwołania się do określonego wiersza i uzyskania dostępu do jego elementów. Jednym ze sposobów jest użycie cał­ kowitej zmiennej indeksowej (w poniższym przykładzie jest to zmienna tvc) w celu reprezentowania wiersza tabeli12. struct vco1 *vc: int tvc:

[...] cnt - vcftvc].cnt.

Jednak w takich przypadkach informacje dotyczące określonej tabeli, do której uzyskuje się dostęp zostają utracone. Wszystko co nam zostaje, to wartość całkowita, która może |~i~1 oznaczać wiele różnych (niezgodnych ze sobą) rzeczy. Lepszym rozwiązaniem jest użycie wskaźnika na określony element struktury w celu reprezentowania zarówno tabeli, jak i należących do niej lokalizacji. Wartość takiego wskaźnika może być zwięk­ szana i zmniejszana w celu poruszania się po tabeli. Można również wyłuskiwać wska­ zywane przez niego elementy za pomocą operatora ->. uzyskując dostęp do pól struktury (listing 4.1:2). W systemach bazodanowych taką abstrakcję określa się mianem kursora (ang. cursor). Macierze są alokowane i przetwarzane przy użyciu innego zestawu technik. Ze względu na fakt, że wszystkie elementy macierzy są tego samego typu, można spotkać wiele sposobów ich przechowywania i przetwarzania. Narzucającym się sposobem jest użycie dwuwymiarowej tablicy w celu przydzielenia pamięci i uzyskiwania dostępu do ele­ mentów13. typedef double Transform3D[4][4]:

[...] IdentMat(Transform3D m)

!

register 1nt i: register Int j: for (1 - 3: 1 >- 0: --1)

(

for (J - 3: j >- 0: --J) - 0 .0 :

m[1][l] - 1.0; 1 |T| Jednak ta metoda nie może być wykorzystana w przypadku macierzy o pamięci przy­ dzielanej dynamicznie. Kiedy macierze mają stały rozmiar lub kiedy liczba kolumn jest znana, można spotkać kod, w którym deklarowane są zmienne wskazujące na kolumny 12

netbsdsrc/usr.sbin/pr/pr.c: 314 - 530.

13XFree86-3.3/conlrib/programs/ico/ico.c: 110- 1151.

108

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

tablicy i pamięć jest przydzielana na podstawie liczby wierszy oraz rozmiaru każdej kolumny. W poniższym przykładzie14 zostaje zadeklarowana zmienna rknots jako ma­ cierz licząca MAX0RD kolumn i dynamicznie zostaje przydzielona pamięć wymagana do przechowania wszystkich numKnots wierszy15. ddFLOAT (*rknots)[MAX0RD]-0: /* reciprocal of knot diff */ if ( !( rknots - (ddFLOAT (*)[MAX0RD]1 xalloc( MAXORD * numKnots * s1zeof(f1oat))) )

Zmienne te mogą być następnie przekazywane do funkcji i używane jako tablice dwu­ wymiarowe (w poniższym przykładzie kr), ponieważ w przypadku tablic używanych w języku C można pominąć pierwszy wymiar16. m1_nu_compute nurb_bas1s_function( order. 1, knots, rknots. C );

[" .] void mi_nu compute nurb_basis_funct1on( order. span, knots, kr. C )

[“ .] ddFLOAT

kr[][MAXORD]; /* reciprocal of knots diff */

[...] (

[...] for ( k - 1: k < order: k++ ) ( tO - tl * kr[span-k+l][k]:

Alternatywne rozwiązanie, używane w przypadku, gdy rozmiar każdego wiersza jest różny (jak ma to miejsce w przypadku macierzy diagonalnych lub symetrycznych), polega na przechowaniu macierzy poprzez oddzielne przydzielenie pamięci dla każdego wiersza. Następnie wykorzystywana jest tablica wskaźników na wskaźniki na elementy w celu przechowywania adresów każdego z wierszy. Zatem posiadając wskaźnik na taką strukturę p możemy określić, że piąty wiersz znajduje się pod adresem *(p + 5), natomiast trzeci element w tym wierszu jest reprezentowany przez *(*(p + 5) + 3). |T | Na szczęście ze względu na fakt, że indeksowanie tablic w języku C może być używane jako skrócona forma wyłuskiwania elementów, na które wskazują wskaźniki z przesu­ nięciem, powyższy zapis jest równoważny następującemu: p[5][3]. Istotną rzeczą jest zauważenie, że p nie jest tablicą dwuwymiarową— zajmuje dodatkową przestrzeń słu­ żącą do przechowywania wskaźników na wiersze i dostęp do elementów jest związa­ ny z przeglądaniem wskaźników, a nie obliczaniem lokalizacji elementu za pomocą mnożenia. Jak widać na listingu 4.217. pamięć elementów dla tej struktury jest przy­ dzielana w dwóch etapach: najpierw tablica wskaźników na każdy wiersz, następnie poszczególne elementy każdego wiersza. Od tego miejsca można wykonywać operacje przypominające uzyskiwanie dostępu do tablic dwuwymiarowych.

14 XFree86-3.3/xc/programs/Xserver/PEX5/ddpex/mi/level2/miNCurve.c: 364 - 5 1 1 .

15 Dave Thomas recenzując rękopis opatrzy! ten fragment następującym komentarzem: „Opisywany kod nie jest zbyt bezpieczny, gdyż typ elementów macierzy nie jest powiązany z kodem wywołania funkcji xal loc używanej do przydzielenia pamięci: deklaracja wykorzystuje typ ddFLOAT, zaś funkcja xalloc wykorzystuje typ float. Jest to dobry przykład kodu, który lepiej byłoby zapisać jako rknots = xalloc(MAXORD * numKnots * sizeof(rknotstO][0])):. 16 XFree86-3.3/xc/programs/Xserver/PEX5/ddpex/mi/level2/miNurbs.c : 154 - 179.

17 XFree86-3.3/xc/programs/Xserver/Xprinl/pcl/Pc!Text.c: 638 - 655.

Rozdział 4. ♦ Struktury danych języka C

109

Listing 4.2. Macierz bazująca na wskaźniku PclFontMapRec * * index: • ----------------------------------------------------------------------------------- Deklaracja macierzy

[.. ] index - (PclFontHapRec **)xalloc- AOBJHJEUE) adblnHeacH):

wyrażenia warunkowego23: static int h1story_head. h1story_tail; Idefine h i st JumpOif ) DrawPoInts (old_plxmaps[13. bit_0_gc. xp. n):

lub działań arytmetycznych modulo24: adb_evq[(adb_evq_1en + adb_evq_tall) % ADB_HAX EVENTS] * *event:

Wiele kolejek implementuje się, korzystając z indeksu początku i końca* . volatile 1nt volatile m t

rnd_head; md_ta1l:

[...] volatile rnd_event_t

rnd_events[RND_EVENTQSIZE]:

W przypadku takich implementacji często indeks początku określa pierwszy element do usunięcia, zaś indeks końca określa pierwszy pusty element. Zatem kolejkę pustą określa ta sama wartość indeksu początku i końca26. /* * check for empty queue

*/ if (rnd_head — return;

rnd_tail)

Z kolei pełną kolejkę określa indeks końca wskazujący na element znajdujący się o jedną pozycję od indeksu początku2'. /* * check for full ring. If the queue 1s full and we have not * already scheduled a timeout, do so here.

*/ nexthead - (rnd_head + 1) S (RNDJVENTQSIZE - 1). if (nexthead — rnd_tall) (

Warto zauważyć, że w opisywanej implementacji elementy są dodawane na początku kolejki i usuwane na jej końcu. Ponadto taki schemat powoduje marnotrawstwo jednego elementu, gdyż tylko zapewniwszy, że zawsze będzie on pusty można odróżnić kolejkę pełną od kolejki pustej. Aby udowodnić, że jest to potrzebne, wystarczy zastanowić się nad tym, w jaki sposób należałoby reprezentować pustą oraz pełną kolejkę składającą się z jednego elementu. Można uniknąć tego problemu poprzez bezpośrednie przecho­ wywanie liczby elementów znajdujących się w kolejce, jak ma to miejsce na listingu 4.528. Taka metoda przechowywania elementów może zostać zoptymalizowana tak, aby przechowywać jedynie informacje o końcu kolejki oraz liczbie elementów'29. static adb_event_t adb_evq[ADB_MAX_EVENTS]; /* ADB event queue */ static Int adb_evq_tail - 0; /* event queue t.a1l */ static int adb_evq_len - 0: /* event queue length */

‘4netbsdsrc/sys/arch/mac68k/dev/adb.c\ 133- 134. "5netbsdsrc/sys/dev/md.c: 99 - 102. 26netbsdsrc/sys/dev/md.c. 826 - 830. "7netbsdsrc/sys/dev/md.c: 163 - 768. ■)Q

netbsdsrc/sys/arch/mac68k/dev/adb direct.c: 239 - 1611.

“9netbsdsrc/sys/arch/mac68k/dev/adb.c. 72 -74.

114

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Listing 4.5. Kolejka z bezpośrednio zdefiniowanym licznikiem elementów struct int int fnt

adbCommand adbInboun^ADB_QUEUE]; 1— Tablica kolejki

adblnCourrt-O; • -----------------------------------Licznik elementów adblnHead-O; • ------------------------------------ Pierwszy element do usunięcia adbInTa11“0; • ------------------------------------ Pierwszy pusty element

void adb_pass_up(struct adbConrnand *in) i f (adbtnCount>-AOB_QłJEui~"(------ *--------- Sprawdzenie przepełnienia kolejki

prlntf_1ntr(”adb- ring buffer overflovAn”): return:

)

..........................

adbInbOUnd[adbinTa1l] cmd»cind; • --------- Dodanie elementu adbInCount++.---------------- • ----------- Skorygowanie wskaźnika końca kolejki oraz licznika elementów 1f (++adbInTa1l >- ADBQUEUE) adblniail-O:

[...] void adb softjntr(void)

{ wh1 )e (adblnCount) {

• — Pętla wykonywana dopóki w kolejce znajdują się jakieś elementr

[■■•] cmd-adb I nbound[adbi nHead] .curi: • — Pobranie jednego elementu

[-■■] adblnC ount--:

• --------Skorygowanie wskaźnika końca kaletki oraz licznika elementów

if (++adb!nHead >- ADB_QUEUE)

H H H i ) Ćwiczenie 4.11. Zlokalizuj dziesięć wystąpień kolejek na płycie CD-ROM dołączonej do książki i zidentyfikuj systemy, w których komunikacji są używane. Ćwiczenie 4.12. Zlicz przykłady z płyty CD-ROM dołączonej do książki, w których ele­ menty są dodawane na początku kolejki oraz te, w których są dodawane na końcu kolejki. Zaproponuj strategię zapobiegającą występowaniu tego rodzaju niejednoznaczności. Ćwiczenie 4.13. W dokumentacji kodu źródłowego wyszukaj słowa „queue” (kolejka) i postaraj się zidentyfikować odpowiednie struktury danych. Zlokalizuj przynajmniej dwa przypadki, w których odpowiednia struktura danych kolejki nie jest używana i uza­ sadnij stosowność takiej strategii.

4.5. Mapy Kiedy używa się zmiennej indeksowej tablicy w celu uzyskiwania dostępu do jej ele­ mentów, wykonywana jest bardzo wydajna operacja, zwykle implementowana przy użyciu jednej lub dwóch instrukcji maszynowych. Ta cecha sprawia, że tablice stanowią idealne rozwiązanie dla tworzenia prostych map (ang. maps) lub tabel przeglądowych I

Rozdział 4. ♦ Struktury danych języka C

115

(ang. lookup tables), w których kluczami są kolejne nieujemne liczby całkowite. W przy­ padkach, gdy elementy tablicy są znane z góry, mogą one być wykorzystane do bezpo­ średniego zainicjalizowania zawartości tablicy3". static const int year_lengths[2] - { DAYSPERNYEAR. DAYSPERIYEAR

}: Tablica zdefiniowana w powyższym przykładzie zawiera tylko dwa elementy, określa­ jące liczbę dni w normalnym i przestępnym roku. Ważny jest nie rozmiar tablicy, ale możliwość swobodnego dostępu do elementów poprzez jej indeks31. Janftrst +- year lengths[1s1eap(year)] * SECSPERDAY:

[...] while (days < 0 || days >- (long) year lengths[yleap * isleap(y)]) {

[...] yourtm.tmjnday +- year_lengths[1sleap(1)]:

Ta sama zasada jest często wykorzystywana w przypadku tabel wielowymiarowych32. static const 1nt mon_lengths[2][MONSPERYEAR] - { { 31. 28. 31. 30. 31. 30. 31. 31. 30. 31. 30. 31 ). ( 31. 29. 31. 30. 31. 30. 31. 31. 30. 31. 30. 31 }

}: W powyższym przykładzie następuje inicjalizacja dwuwymiarowej tablicy, w której są przechowywane liczby dni każdego miesiąca w roku normalnym i przestępnym. Czy­ telnik może uznać, że informacje przechowywane w tablicy monjengths są banalne p~] i jest to marnotrawstwo pamięci. Nie jest tak jednak — użycie tablic w taki sposób po­ zwala często na wydajne kodowanie struktur sterujących,co upraszcza logikę progra­ mu. Rozważmy sytuację, w którejponiższy fragment kodumusiałby zostać zapisany przy użyciu instrukcji i f33. value +- mon_lengths[leapyear][1] * SECSPERDAY:

[...] If (d + DAYSPERWEEK >- mon_lengths[leapyear][rulep->r_mon - 1])

[...] 1 « nran_lengths[1sleap(yourtm.tm_year)][yourtm.tm_mon]:

Naturalnym rozszerzeniem metody użycia tablicy w celu zakodowania elementów ste­ rujących programu jest zastosowanie podejścia w pełni opartego na tabelach. W naj­ częściej występującym przypadku tablice są używane do kojarzenia danych z kodem poprzez przechowywanie na każdej pozycji elementu danych oraz wskaźnika na funkf n cję przetwarzania elementu. W przykładzie z listingu 4.63'1 wykorzystywana jest tablica zainicjalizowana nazwami poleceń oraz wskaźnikami na funkcje używane do wyko­ nywania odpowiednich poleceń. Funkcja ksearch przeprowadza wyszukiwanie binarne względem nazwy polecenia na zawartości tablicy (posortowanej według nazw poleceń w porządku rosnącym). Jeżeli zostanie znaleziony wpis dotyczący danego polecenia, 30 netbsdsrc/lib/libc/time/localtime.c: 453 - 455. 31 netbsdsrc/lib/libc/time/localtime.c: 832 - 1364. 3" netbsdsrc/lib/libc/time/localtime.c. 448 - 451. 33 netbsdsrc/lib/libc/time/localtime.c: 682 - 1373.

33netbsdsrc/bin/stty/key.c: 7 4 - 148.

116

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

funkcja ksearch wywołuje odpowiednią funkcję. Uogólniając powyższe przykłady można stwierdzić, że tablice umożliwiają sterowanie działaniem programu poprzez przecho­ wywanie danych lub kodu używanego przez maszyny abstrakcyjne (ang. abstract ma­ chines), inaczej wirtualne (ang. virtual), zaimplementowane w programie. Typowe przykłady takiego podejścia to implementacje analizatorów składniowych i leksyka nych przy użyciu automatów stosowych lub realizacje interpreterów oparte na wyko­ nywaniu instrukcji maszyny wirtualnej. Listing 4.6. Działanie programu bazujące na tablicach____________________________________ I static struct key { char *name: • -------------------------------------------------void (*!)(struct 1nfo *); |--------------------------------------

Nazwapolecenia ! 'totkiii< >przetworzenia

[...] * ) keys[] - { { -all*. f_all. 0 }, • -------------------------{ "cbreak". f cbreak. F OFFOK ).

iabeia poleceń i funkcji

[...] I "tty".

f_tty.

0 },

): [...] 1nt

ksearchtchar ***argvp, struct Info *lp)

{

char *name; struct key *kp. tmp: name * **argvp;

[...] tup.name - name: if (!(kp - (struct key *)bsearch(Stmp. keys. •s1zeof(keys)/slzeof(struct key), slzeoftstruct key). c_key))) return (0);

t-.] kp->f(1p); • --------------------------------------------------return (1);

Wyszukanie /miecenia w tablicy

-Wykonanieodpowiedniej funkcji

) void f alKstruct info *1p)

f

• -----------------------

i or.K: :oprzetwarzająca dla polecenia "all"

prtnt(&1p->t, Slp-^win. ip->ld1sc. BSD):

)

m

Mniej ważny, ale często spotykany idiom występujący na listingu 4.6 ma związek z ob­ liczaniem liczby elementów tablicy key. W momencie definiowania tablicy key jej roz­ miar nie zostaje określony — kompilator ustalają na podstawie liczby elementów użytych do jej zainicjalizowania W programie, w' miejscach, w których potrzebna jest znajomość liczby elementów tablicy, może ona zostać otrzymana jako W7 nik wyrażenia sizeof (keys)/sizeof(struct key). Wyrażenie postaci sizeof(x)/sizeof(x[0]) zawsze moż-j na odczytać jako liczbę elementów tablicy x. Liczba elementów jest obliczana jako stała czasu kompilacji poprzez podzielenie rozmiaru wymaganego dla całej tablicy przez rozmiar wymagany dla każdego z jej elementów. Ze względu na fakt, że wynik tej ope­ racji jest stalą czasu kompilacji, często można spotkać jej użycie w określenia rozmiaru lub inicjalizacji innych tablic35.

35 netbsdsrc/sys/netinet/in_proto.c: 167 - 177.

Rozdział 4. ♦ Struktury danych języka C

117

struct protosw impsw[] - ( { SOCK RAW. &1mpdoma1n. 0. PR ATOHIC|PR_AODR.

[....] )• J: struct domain impdomain { AF_IHPLINK. "imp". 0. 0. 0. impsw. &1mpsw[sizeof sb,st ino) S L_TAB_SZ: 1f (tpt - ltab[indx]) !- NlILO {

W powyższym przykładzie wartość st_i no jednoznacznie identyfikująca plik (określana mianem numeru i-węz la) jest używana jako indeks tablicy Itab zawierającej L_TAB_SZ elementów. Wynik działania modulo zawsze odpowiada dokładnie zakresowi dopusz­ czalnych indeksów tablicy, a stąd jest bezpośrednio używany w celu uzyskiwania dostępu do jej elementów. Często kosztów związanych wykonaniem działania modulo można uniknąć zapewniając, że tablica zawsze będzie zawierać liczbę elementów równą potędze 2 i używając operacji bitowego iloczynu logicznego, co opisano w podrozdziale 2.10. Kiedy zmienna indeksowa, której chcemy użyć w mapie, nie jest wartością całkowitą, zamienia się ją na taką wartość za pomoc ^funkcji mieszającej (ang. hash function). Funkcja mieszająca łączy elementy danych klucza w taki sposób, aby móc je zamienić na wartość całkowitą. Przykładowo, funkcja mieszająca z listingu 4.73 konwertuje ciąg znaków na wartość całkowitą, używając funkcji alternatywy wykluczającej (X0R) na każdym znaku ciągu z dotąd wyliczoną wartością mieszającą przesuniętą w lewo o trzy pozycje. Wyniki ujemne są zamieniane na dodatnie. Przed dokonaniem przeglądu tabeli obliczona wartość jest ponownie ograniczana do zakresu dopuszczalnych w'artości indek­ su tablicy poprzez użycie bitowego iloczynu logicznego (listing 4.7:2). Należy pamiętać, że wiele różnych wartości indeksu mapy może po zastosowaniu funkcji mieszającej

16 netbsJsrc/bin/pax/lables.c: 163 — 167. ,7XFree86-3.3/xc/lib/font/util/atom.c: 5 5 - 173.

118

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

odpowiadać tej samej pozycji. Stąd po zlokalizowaniu pozycji w tablicy należy doko­ nać sprawdzenia, czy element znajdujący się na tej pozycji rzeczywiście jest poszu-1 kiwanym (listing 4.7:3). Końcowe komplikacje wynikają z faktu, że może zachodzić potrzeba przechowywania na tej samej pozycji więcej niż jednego elementu. Często wykorzystywaną w takim przypadku strategią jest obliczenie pozycji w tablicy nowych elementów kandydujących poprzez użycie innej funkcji (listing 4.7:4). Owe alternatyw­ ne pozycje są następnie wykorzystywane do rozmieszczenia elementów w przypadku, gdy ich główne pozycje okażą się już zajęte, oraz do wyszukiwania elementów w przy- j padku, gdy główna pozycja nie zawiera poszukiwanego elementu (listing 4.7:5). Listing 4.7. F u n kc ja m iesza ją ca o ra z dostęp do ta b licy m ieszającej static Hash(char *str1ng. 1nt len)

{

1nt

• -----

- [ 11 p unki ja mieszająca

h:

h - 0;

while (1en--) h - (h « 3) * *str1ng*+; if Ch < 0) return -h: return h:

) [

]

Atom MakeAtomichar *strlng. unsigned len. int makelt)

{

int

hash. h. r;

hash - Hash (string, len): -O bliczenie wartości mieszającej if (hashTable) { h - hash & hashMask: < ~[2j Ograniczenie wartości mieszającej Jo rozmiaru tablicy j if (hashTable[h)) { [3 ] Spraw dzenie czy znaleziono if (hachTableCh] >hash — hash && odpowiedni element hashTable[h]->len — len M NameEqual (hashTable[h]->name. string, len)) return hashTable[h]->atom.

r - (hash

t

rehash) | 1:

[4] Wartość służąca do obliczenia alternatywnych pozycji

for (:;) ( # h r. if (h >— hashSIze) h -- hashSIze: if CihashTableth]) break, 1f (hashTable[h]->hash — hash && hashTable[h]->len — len && NameEqual (hashTable[h]->name. string, len)) return hashTable[h]->atom:

[SJ Wyszukanie alternatywnych pozycji I

)

Ćwiczenie 4.14. Zlokalizuj na płycie CD-ROM dołączonej do książki przypadek, w któ­ rym tablica jest używana do kodowania informacji sterujących. Przepisz kod bezpo­ średnio używając instrukcji sterujących. Określ różnicę pod względem liczby wierszy! kodu i skomentuj względne możliwości konserwacji, rozszerzalność oraz wydajność | obu rozwiązań.

119

Rozdział 4. ♦ Struktury danych języka C

Ćwiczenie 4.15. Zlokalizuj fragment kodu. w którym struktury sterujące można zastąpić mechanizmem przeglądania tablicy. Wyjaśnij, w jaki sposób należałoby zaimplemen­ tować kod. Ćwiczenie 4.16. Zlokalizuj na płycie CD-ROM dołączonej do książki co najmniej trzy różne implementacje funkcji mieszającej operującej na ciągach znaków. Wybierz repre­ zentatywny zbiór wejściowych ciągów znaków i porównaj implementacje pod wzglę­ dem liczby elementów, które kolidują ze sobą pasując do tej samej pozycji w tablicy. Porównaj również czas wykonania każdej z funkcji. Ćwiczenie 4.17. Czy można przewidywać jakieś trudności w implementacji mapy mieszającej jako abstrakcyjnego typu danych? Jeśli tak, zaproponuj sposoby ich po­ konania.

4.6. Zbiory Istnieją przypadki, w których chcemy w wydajny sposób reprezentować i przetwarzać zbiory elementów. Kiedy elementy te można wyrazić jako względnie nieduże warto­ ści całkowite, zwykle spotykane implementacje opierają się na reprezentowaniu zbioru jako tablicy bitów, gdzie przynależność do zbioru każdego elementu zależy od warto­ ści określonego bitu. Język C nie posiada typu danych służącego do bezpośredniego [T) reprezentowania i adresowania bitów w formie tablic. Z tego względu w programach wykorzystuje się jeden z typów całkowitych (char, in t) jako sposób przechow ywania elementów i adresuje się odpow iednie bity używając przesunięć oraz bitowych operato|T1 rów AND i OR. Przykładowo, w poniższym wierszu kodu do zbioru pbitvec zostaje wsta­ wiony element j 38. pbUvec[j/BITS_PER_LONG]

((unsigned longlt «

(j i B1TS_PER_L0NGi):

Tablica pbitvec składa się z liczb całkowitych typu long, z których każda zawiera BITS_PER_LONG bitów. Każdy element tablicy może zatem przechowywać informacje o BITS_PER_LONG elementach zbioru. Tak więc dzielimy numer elementu zbioru j przez BITS_PER_LONG w celu znalezienia pozycji w tablicy, która zawiera informacje o okre­ ślonym elemencie, a następnie używamy reszty z dzielenia do przesunięcia w lewo bitu o wartości 1 w celu umieszczenia go na pozycji, na której są przechowywane informa­ cje o określonym elemencie. Wykonując operację bitowej sumy logicznej na istniejącej wartości tablicy i wartości utworzonego bitu. ustawiamy ten bit na wartość I. co oznafTI cza, że j należy do zbioru. Podobnie można sprawdzić, czy dany element należy do zbioru przy użyciu binarnego iloczynu logicznego na skonstruowanym bicie oraz od­ powiednim elemencie tablicy39. #define FDJSSETin. pj \ (fds_bits[(n)/NFDBITS] & (1 «

38

next - source; source - s:

[...] source - s->next:

/* pop source stack */

Rysunek 4.1. Lista jednokierunkowa Listing 4.8. Definicja listyjednokierunkowej oraz podstawowych operacji struct hostjlst f struct hostjist *next: struct in_addr addr: •) ‘hosts; • --------------1nt search_host(struct inaddr addr)

( 43

netbsdsrc/usr.bin/rup/rup.c: 60 - 97.

44netbsdsrc/bin/ksh/lex.c: 639 - 644, 845.

—f l ] Następny element listy -[2 ] Dane węzła listy - f 3 ] Głowa listy -W yszukanie elementu w liście

122

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source struct hostjist *hp:

for Chp - hosts; hp !- NULL: hp - hp->next) ( • 1f (hp->addr.s_addr — addr.s_addr)• -----B retum:

-[4 ] Iteracyjne przejrzenie elementów listy -E lem ent znaleziono

} return(O)

---------------------------------

-Elementu nie znaleziono

void reraember_host(struct ln_addr addr)

-Dodanie elementu do listy

{ struct hostjist *hp: 1f (!(hp - (struct hostjist *)malloc(sizeof(struct hostjist)))) { err(l. "malloc"): /* NOTREACHED */ hp->addr s addr - addr.s_addr:« hp->next - hosts:»----------hosts - hp:

-Przechowanie danych

]

- f t ] Dołączenie elementu do listy

Usunięcie dowolnego elementu z listy jest bardziej skomplikowane, gdyż potrzebna jest zmienna wskazująca na wskaźnik określonego elementu. W tym celu używany jest wskaźnik na wskaźnik listy, jak w poniższym przykładzie45. STATIC int unaliastchar *name)

{

struct alias *ap. **app: app - hashalias(name); for (ap - *app; ap: app - &(ap->next), ap - ap->next) { if (equal(name. ap->name)) {

[...] *app - ap->next: ckfree(ap->name): ckfree(ap->val): ckfree(ap):

[...] return (0):

) }

return (1):

W powyższym przykładzie zmienna app początkowo wskazuje na głowę listy. Zmienna ap jest używana w celu przejścia przez listę, natomiast wskaźnik app zawsze wskazuje na lokację, w której jest przechowywany wskaźnik bieżącego elementu. Stąd, kiedy zostanie znaleziony element, który ma zostać usunięty, wskaźnik *app ustawia się tak, aby wskazywał na następny element, co w efekcie powoduje usunięcie z listy znalezio­ nego elementu. Należy zauważyć, że w poprzednim przykładzie po usunięciu elementu nie można było I użyć zmiennej ap w celu przejścia do następnego elementu, gdyż zajmowana przez nią 45 netbsdsrc/bin/sh/alias.c: 120- 152.

Rozdział 4. ♦ Struktury danych języka C

123

A pamięć (w tym wskaźnik

ap->next) została zwolniona. Próba uzyskania dostępu do składowej next usuniętych elementów listy jednokierunkowej jest często popełnianym błędem46. for (; ihead !- NULL: ihead • lhead->nextp) ( free(ihead): if ((opts & 1GNLNKS) || ihead->count — 0) continue; if (Ifp) logCIfp. "is: Warning: missing I1nks\n". ihead->pathname);

) W powyższym przykładzie dostęp do wskaźników ihead->nextp oraz ihead->count następuje po zwolnieniu pamięci zajmowanej przez ihead. Choć kod ten może działać poprawnie w przypadku niektórych architektur i implementacji bibliotek języka C, dzieje się tak tylko przez przypadek, gdyż zawartość zwolnionych bloków pamięci nie umoż­ liwia przyszłego dostępu. Takie błędy występują szczególnie często w przypadku usu­ wania całej listy. Poprawny sposób usunięcia takich elementów polega albo na użyciu tymczasowej zmiennej służącej do przechowywania adresu każdego kolejnego elementu4 for (j - jobjist: j: j - tmp) { tmp - j->next: if (j->flags & JF_REM0VE) remove_job(J. "notify”);

} albo na przechowaniu w tymczasowej zmiennej adresu elementu, który ma zostać zwolniony, przed przesunięciem wskaźnika listy48. struct alias *ap. *tmp;

[...] while (ap) { ckfree(ap->name): ckfree(ap->val): tmp - ap; ap - ap->next; ckfree(tmp):

) Jest wiele różnych rodzajów list. Listy jednokierunkowe, o których dotąd była mowa, występują najczęściej. Jedna z odmian jest związana z wiązaniem każdego elementu listy zarówno z jego poprzednikiem, jak i następnikiem. Taka lista nosi nazwę listy dwukierunkowej (ang. doubly linked list) — patrz rysunek 4.2. Element, którego istnie­ nie pozwala natychmiast stwierdzić występowanie listy dwukierunkowej, to wskaźnik o nazwie prev.4 . struct queue { struct queue *q next. *q_prev:

}:

46netbsdsrc/usr.bin/rdist/docmd.c'. 206 - 213. 47netbsdsrc/bin/ksh/jobs.c: 1050- 1054. 48netbsdsrc/bin/sh/alias.c: 164- 177. 49

netbsdsrc/sys/arch/arm32/arm32/stubs.c: 82 - 84.

124

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 4.2. Lista dwukierunkowa

Dwie korzyści wynikające z wykorzystania list dwukierunkowych to (1) możliwość wstawiania i usuwania elementów na dowolnej pozycji listy bez konieczności okre­ ślania jakichkolwiek innych informacji poza wskaźnikiem na element, do którego ma nastąpić wstawienie lub usunięcie oraz (2) możliwość przechodzenia listy w tył. Listy dwukierunkowe są często używane w celu implementacji kolejek dwukierunkowych (ang. double-ended queue, deque). Są to kolejki, w których elementy mogą być doda­ wane lub usuwane na obu końcach. Przechodzenie listą dwukierunkową w kierunku wstecznym wykonuje się zwykle uży­ wając pętli for50. for (p2 - p2->prev: p2->word[0] !=

p2 - p2->prev)

Operacje dodawania i usuwania elementów z listy dwukierunkowej mogą wydawać się skomplikowane, jednak można z łatwością je zrozumieć rysując prostokąty reprezentu­ jące elementy oraz strzałki reprezentujące zmieniane wskaźniki. Rozważmy poniższy fragment kodu, który dodaje węzeł listy elem za węzłem head51. register struct queue *next; next - head->q_next: elem->q_next - next: head->q_next - elem: elem->q_prev - head: next->q_prev - elem:

Kolejne etapy zilustrowane na rysunku 4.3 wyraźnie pokazują w jaki sposób wskaźniki struktury danych są kolejno modyfikowane, tak aby dołączyć węzeł e'em do listy. W po­ wyższym przykładzie wykorzystywana jest tymczasowa zmienna next, pozwalająca na F I uproszczenie wyrażeń zawierających wskaźniki. W innych przypadkach sąsiednie ele­ menty listy są bezpośrednio wyrażane przy użyciu wskaźników next oraz prev elementu zaczepienia52. ep->prev->next - ep->next: if (ep->next) ep->next->prev - ep->prev:

Choć taki kod może początkowo wydawać się skomplikowany, chwila zastanowienia i użycie ołówka i kawałka papieru pozwala szybko odkryć jego działanie (powyższy przykład służy do usunięcia elementu ep z listy dwukierunkowej).

50 netbsdsrc/bin/csh/parse.c: 170. 51 netbsdsrc/sys/arch/arm32/arm32/stubs.c: 96 - 102. 52

-

netbsdsrc/usr.bin/telnet/commands.c: 1 7 4 5 - 1747.

Rozdział 4. ♦ Struktury danych języka C

125

Rysunek 4.3. Dodanie elementu do Przed wstawieniem

.prev

•head

.next

prev

*elem

next

.prev

‘ next

next

elem->q_next = next

.prev

•head

next

.prev

*elem

.next

prev

•next

.next

head*>q_next = elem

.prev

•head

next

prev

•elem

next

.prev

’ next

next

elem->q_prev = head

.prev

•head

.next

.prev

•elem

next

.prev

•next

next

next->q_prev = elem

prev

•head

next

.prev

•elem

next

.prev

•next

next

^

7

^

7 * \^

Niekiedy ostatni element listy zamiast zawierać wartość NULL wskazuje na pierwszy element listy (rysunek 4.4). Taka lista jest określana mianem listy cyklicznej (ang. cir­ cular lisi) i jest często używana do implementowania buforów pierścieniowych lub innych struktur, które charakteryzuje cecha zawijania zawartości. Listy cyklicznej nie można przechodzić korzystając z pętli for, gdyż nie istnieje element, który określałby jej zakończenie. Zamiast tego można spotkać kod przechodzenia listy cyklicznej, prze­ chowujący w zmiennej pierwszy element, do którego uzyskuje się dostęp i wykonuje pętlę do momentu ponownego osiągnięcia tego elementu53. void prlex(FILE *fp, struct wordent *spO)

f

struct wordent *sp - spO->next: for (::) { (void) fprintf(fp. ”*s". vis_str(sp->word)): sp - sp->next: if (sp — spO) break; if (sp->word[0] !- ’\n') (void) fputcC '. fp):

} 1 Rysunek 4.4. Lista cykliczna

Powyższy przykład drukuje zawartość listy cyklicznej rozpoczynając od pozycji sp i używając zmiennej sO jako znacznika wyjścia z pętli. Listy są również używane w przypadku przechowywania więcej niż jednego elementu na pojedynczej pozycji tablicy mieszającej, co zilustrowano na rysunku 4.5. W czasie szukania elementu w mapie funkcja mieszająca w wydajny sposób lokalizuje listę, którą 53netbsdsrc/bin/csh/lex.c: 187 - 202.

Czytanie kodu. Punkt widzenia twórców oprogramowania operesource

126

Rysunek 4.5.

- » j Ciąg znaków | next |

Tabela mieszająca z listami

- ^ | Ciąg znaków | next [-

Ciąg znaków | neirt

- ^ | Ciąg znaków | next |

-S»j Ciąg znaków | next |---------- 5»-| Ciąg znaków | ne»l | Ciąg znaków | next |

Tabela mieszająca

należy przeszukać, i proste przejście tej listy pozwala na zlokalizowanie konkretnego elementu54. struct wllst * lookup(char *s)

I

struct wllst *wp: for (wp - hashtab[hash(s)]; wp !- NULL, wp • wp->next) 1f (*s — *wp->string && strcmpts. wp->string) — 0) return wp: return NULL:

) Taka struktura oferuje wydajny sposób uzyskiwania dostępu do elementów danych (identyfikatorów) przy względnie niedużych kosztach implementacyjnych. Jest ona często używana do konstruowania tabel symboli (tabele symboli są często wykorzysty­ wane w procesorach języków, takich jak interpretery i kompilatory, w celu przechowy­ wania i pobierania szczegółowych informacji o określonym identyfikatorze, na przy­ kład typie danej zmiennej). Ćwiczenie 4.20. Zlokalizuj na płycie dołączonej do książki pięć przypadków użycia list jedno- oraz dwukierunkowych i wyjaśnij pełnione przez nie funkcje. Ćwiczenie 4.21. Poniższa funkcja jest wywoływana ze wskaźnikiem na element listy dwukierunkowej” . void remquefvoid *v)

T

register struct queue *elem - v: register struct queue »next. *prev; next - etem->q_next: prev • elem->q_prev: next->q_prev - prev: prev->q_next - next: elem->q_prev - 0:

) Rysując schemat, wyjaśnij wykonywane działania. 54 netbsdsrc/games/battlestar/parse.c: 70 - 80. ,5 netbsdsrc/sys/arch/armi2/arm32/stubs.c\ 1 0 9 - 121.

Rozdział 4. ♦ Struktury danych języka C

127

4.8. Drzewa Wiele algorytmów i metod organizacji informacji bazuje na użyciu drzew (ang. trees) jako struktury danych. Formalna definicja drzewa określa, że są to wierzchołki połączo­ ne krawędziami w taki sposób, że istnieje dokładnie jedna droga z każdego wierzchołka do korzenia drzewa. Sposób, w jaki wierzchołki drzew rozszerzają się na każdym po­ ziomie, jest często wykorzystywany w celu wydajnego organizowania i przetwarzania danych. Wystarczy zauważyć, że drzewo binarne o głębokości 20 poziomów (posiada­ jące 20 wierzchołków na drodze z najniższego poziomu do korzenia) może przechowy­ wać około miliona (220) elementów. Wiele algorytmów wyszukiwania56, sortowania 7. przetwarzania języków 8, graficznych59 oraz kompresji danych6" wykorzystuje drzewia­ ste struktury danych. Ponadto drzewa są używane do organizacji plików baz danych61, katalogów6 , urządzeń63, hierarchii pamięci , właściwości (na przykład rejestr systemu Microsoft Windows lub domyślne specyfikacje systemu X Window), tras sieciowych65, struktur dokumentów66 oraz elementów wyświetlanych6'. W językach, które obsługują wskaźniki (Pascal, C, C++) lub odwołania do obiektów (Java. C#), drzewa są zwykle implementowane poprzez połączenie wierzchołka nad­ rzędnego z jego potomkami. Określa się to, korzystając z rekurencyjnej definicji typu, według której drzewo jest deklarowane jako składające się ze wskaźników lub referen­ cji na inne drzewa. W poniższym fragmencie kodu jest definiowane drzewo binarne (ang. binary tree): każdy jego wierzchołek posiada co najwyżej dwa potomki65. typedef struct tree_s ( tree_t data: struct tree_s *left. *r1ght; short bal:

1

tree:

Wierzchołkom drzew binarnych zwykle nadaje się nazwy le f t (lewy) oraz right (prawy) w celu podkreślenia pozycji, jakie zajmują w drzewie. Drzewa binarne są często uży­ wane w celu wydajnego sortowania i przeszukiwania danych, dla których można określić 56 netbsdsrc/games/gomoku/pickmove.c. 57 netbsdsrc/usr.bin/ctags/ctags.c. 58 59

netbsdsrc/bin/sh/eval. c. XFree86-3.3/xc/programs/Xserver/mi/mivaltree.c.

60 netbsdsrc/lib/libz/injblock. c. 61 netbsdsrc/lib/libc/db/btree. 62 netbsdsrc/usr. bin/find/fmd. c. 63 netbsdsrc/sys/dev/isapnp/isapnpres. c. 64 XFree86-3.3/xc/programs/Xserver/hw/xfree86/accel/cache/xf86bcache.c. 65 netbsdsrc/sbinJrouted/radix.c. 66 XFree86-3.3/xc/doc/specs/PEX5/PEX5. l/SI/xrefc. 67XFree86-3.3/xc/programs/Xserver/dix/window. c. 68 netbsdsrc/usr.sbin/named/namedUtree.h. 34 - 39.

128

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

pewien porządek. Przedstawiona deklaracja jest używana przez internetowy serwer nazw named w celu organizacji adresów internetowych. Drzewo jest konstruowane w taki sposób, aby wierzchołki o wartości większej niż wierzchołek będący rodzicem zawsze znajdowały się po jego prawej stronie, zaś wierzchołki o niższej wartości — po lewej. Przykład takiego drzewa przedstawiono na rysunku 4.6. Rysunek 4.6.

left 10.0.0.14

Drzewo binarne adresów internetowego senvera nazw

right

Z .le n

o

Ö Ö

/ ,lefl

\ •right

10.0.0.8

1

■right

len

t

'1

10.0.0.12 •right

left 10.0.0.9

right

len 10.0.0.12S

right

■len

10.0.0.135

left 10.0.0.134

right

•right

left 10.0.0.136 .right

j j Rekurencyjna definicja drzewiastej struktury danych umożliwia wykorzystywanie algoT] rytmów rekurencyjnych. Ze względu na fakt, że algorytmy te są bardziej skomplikowa­ ne niż jednowierszowe fragmenty kodu używane do przetwarzania list (podrozdział 4.7), zwykle zapisuje się je w postaci funkcji, która pobiera jako parametr inną funkcję, okre­ ślającą operację wykonywaną na strukturze. W przypadku języków obsługujących pa­ rametryzację typów, takich jak Ada, C++ lub Eiffel, ten sam efekt można w bardziej wydajny sposób osiągnąć używając jako argumentu szablonu. W przypadku drzew binarnych owa funkcja zewnętrzna określa porządek elementów drzewa. Zatem w poniż­ szym przykładzie ta sama struktura drzewiasta oraz funkcje przetwarzające są używane względem drzew zawierających adresy IP. jak i drzew zawierających nazwy komputerów. W pierwszym przypadku funkcja porównująca pfi_compare porównuje całkowite re­ prezentacje adresów IP; w drugim przypadku porównywane są ciągi znaków. Funkcja wyszukująca element p_user w drzewie, na które wskazuje ppr_tree, ma następującą postać69. tree_t tree_srch(tree **ppr_tree. 1nt (*pfi compareM). tree_t p_user)

{ register int

1_comp:

ENTERI”tree_srch") if (*ppr_tree) { 1_comp = (*pfi_compare)(p_user. (**ppr_tree).data); if (i_comp > 0) RET(tree_srch(&(**ppr_tree).right. pfi_compare. p_user)) if (i_comp < 0) RET{tree_srch(&(**ppr_tree).left. pfi_compare. pjjser)) /* not higher. not lower ,. this must be the one.

*/

RET((**ppr treeł.data)

1

/* grounded. NOT found.

*/

RET(NULL)

1 69 netbsdsrc/usr.sbin/named/named/tree.c: 98 - 129.

Rozdział 4. ♦ Struktury danych języka C

129

Wszystko, co wykonuje funkcja, to porównanie elementu z bieżącym elementem drzewa i rekurencyjne przeszukanie wierzchołków leżących po stronie lewej lub prawej lub zwrócenie wierzchołka, w którym znaleziono element. Pierwsza instrukcja if sprawdza, czy drzewo nie jest puste, co zapewnia możliwość zakończenia funkcji. Inną często wykonywaną operacją na drzewach jest systematyczne odwiedzenie wszyst­ kich jego elementów. Takie p rzechodzenie (ang. traversal) po drzewie zwykle koduje się w sposób rekurencyjny i często parametryzuje używając funkcji, która określa dzia­ łania wykonywane na każdym wierzchołku. W poniższym przykładzie7" funkcja tree_ trav wywołuje funkcję pfi_uar w przypadku każdego wierzchołka. Działa ona poprzez rekurencyjne odwiedzenie lewego wierzchołka, wywołanie funkcji pfi_uar, a następnie rekurencyjne odwiedzenie prawego wierzchołka. Jak zwykle, odpowiednie sprawdze­ nie wykonywane na początku kodu powoduje natychmiastowe wyjście z funkcji, jeżeli okaże się, że drzewo jest puste. int tree_trav(tree **ppr_tree. int (*pf1_uar)())

(

[...] if (!*ppr_tree) RET(TRUE) 1f (!tree_trav(&(**ppr_tree).left, pfi_uar)) RET(FALSE) if C!(*pfi_ua r)((**ppr_tree).data )) RET(FALSE) if (¡tree trav(&(**ppr_tree).right. pfi_jarl) RET(FALSE) RET(TRUE)

)

A

Nie będziemy prezentować przykładów kodu służącego do wstawiania i usuwania ele­ mentów z drzewa, ponieważ po prostu istnieje zbyt wiele odmian takich funkcji. Istnie­ jące implementacje sięgają od częściowych (wiele drzew podlega jedynie wzrostowi i żadne ich elementy nie są usuwane), przez banalne, po wyszukane (wykorzystujące na przykład algorytmy zapewniające zrównoważenie drzew w celu zapewnienia efektywności działania algorytmu). Należy pamiętać, że ze względu na fakt, iż drzewa w większości przypadków są budowane zgodnie z określonymi regułami opartymi na uporządkowaniu ich elementów, modyfikowanie danych elementu w miejscu może powodować naruszenie poprawności drzewa. Algorytmy działające na takich drzewach mogą zwracać nieprawidłowe wyniki lub jeszcze bardziej naruszać uporządkowanie elementów drzewa. Innym, również dość częstym obszarem zastosowania drzew jest reprezentowanie struktur syntaktycznych różnych języków. Wiele zadań związanych z przetwarzaniem informa­ cji formalnie określa się w kontekście ję zy k a (ang. language). Nie musi to być w pełni rozwinięty język programowania ogólnego przeznaczenia. Interfejsy do programów działających z poziomu wiersza poleceń, wyrażenia arytmetyczne, języki makrodefinicji, pliki konfiguracyjne, standardy wymiany danych oraz wiele formatów plików jest często określanych i przetwarzanych przy użyciu tych samych standardowych tech­ nik. Zbiór elementów (tokenów ; ang. tokens) jest podd a w a n y analizie składniow ej (ang.

70netbsdsrc/usr.shin/named/named/tree.c: 164 - 181.

130

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

parsing) w celu zbadania ich wzajemnych powiązań. Takie relacje, zwykle określane w kontekście gramatyki (ang. grammar), są następnie reprezentowane w postaci drzewa analizy składniowej (ang .parse tree). W celu uniknięcia komplikacji związanych z przykładem dotyczącym złożonego języka, poniżej zademonstrujemy, w jaki sposób program lint służący do weryfikacji popraw­ ności programów' przechowuje wyrażenia języka C w postaci drzewa. Struktura drze­ wiasta zdefiniowana dla W7 rażeń języka C to znana nam już definicja rekurencyjna '1. typedef struct tnode ( op t tn op: /* operator */

[." ] union ( struct ( struct tnode *_tn_left: /* (left) operand */ struct tnode *_tn_ngbt. /* right operand */ ) tn_s: sym_t *_tn_sym; /* symbol if op — NAME */ val_t *_tn_val: /* value 1f op — CON */ strg_t *_tn_strg; /* string if op =— STRING */ 1 tn_u: ) tnode_t:

Wyrażenia są reprezentowane jako wierzchołki z operatorem przechowywanym w polu tn_op oraz lewym i prawym operandzie przechowywanych w polach, odpowiednio, _tn_left oraz_tn_right72.Na inne elementy (na przykład symbole, wartości stałe, oraz

ciągi znaków) wskazują różne elementy drzewa. W przeciwieństwie do drzewa binar­ nego, wierzchołki są ustawione nie w'edlug ustalonego porządku wartości operatora, ale według sposobu, w jaki są używane wyrażenia w kodzie źródłowym poddawanym analizie składniowej przez program lint. Stąd wyrażenie77 kp->flags & FJIEEDARG && !(ip->arg - -*>+*argvp)

jest reprezentowane przez drzewo analizy składniowej przedstawione na rysunku 4.7. Jak można tego oczekiwać, drzewa analizy składniowej są również często przetwarzane |~i~l przy użyciu algorytmów rekurencyjnych. Są one także często generowane w wyniku rekurencyjnego rozpoznania części języka, przy użyciu rekurencyjnego parsera zstę­ pującego74 lub specjalizowanego generatora parserów \ takiego jak yacc. Ćwiczenie 4.22. Narysuj drzewo binarne, zawierające nazwy domenowe maszyn z Two­ jej elektronicznej książki adresowej. Dodawaj nazwy w sposób losowy, a nie alfabetycz­ ny. W systematyczny sposób przejdź przez drzewo w celu otrzymania posortowanej listy nazw domenowych maszyn. Skonstruuj nowe drzewo, dodając nazwy domenowe maszyn w kolejności, w jakiej występują na skonstruowanej liście. Skomentuj wydaj­ ność wyszukiwania danych w obu drzewach.

1netbsdsrc/usr.bin/xlint/lintl/liml.h: 265 - 280. Należy zauważyć, że standard ANS1C zabrania używania wiodących znaków podkreślenia identyfikatorów w programach użytkownika. 73 netbsdsrc/bin/stty/key.c: 135. 1^ netbsdsrc/bin/expr/expr. c. 75 netbsdsrc/usr.bm/xlmt/lintl/cgram.y.

Rozdział 4. ♦ Struktury danych języka C

131

Rysunek 4.7. Drzewo analizy składniowej wygenerowane przez program lim dla wyrażenia kp->jlags & » F NEEDARG && » !(ip->arg = *+*++*argvp)

Ćwiczenie 4.23. Przeczytaj o drzewach AVL w podręczniku z zakresu algorytmiki i prześledź odpowiednie operacje na podstawie jednej z implementacji znajdujących się na płycie dołączonej do niniejszej książki. Ćwiczenie 4.24. Zlokalizuj na płycie dołączonej do książki przypadki, w których gene­ rowane są drzewa analizy składniowej. Narysuj reprezentatywne drzewo dla określonych danych wejściowych.

4.9. Grafy G raf (ang. graph) jest definiowany jako zbiór wierzchołków (ang. rertices) bądź węzłów (ang. nodes) połączonych krawędziami (ang. edges). Taka definicja jest bardzo ogólna i uwzględnia takie struktury organizacji danych jak drzewa (grafy skierowane nie po­ siadające cykli), zbiory (grafy nie posiadające krawędzi) oraz listy (grafy skierowane o dokładnie jednej krawędzi wiodącej do każdego wierzchołka). W niniejszym podroz­ dziale zostaną zbadane przypadki, które nie należą do wymienionych kategorii. Niestety, ogólny charakter grafowej struktury danych oraz wiele wymagań związanych z progra­ mami wykorzystującymi je sprawia, że istnieje ogromna liczba sposobów przechowy­ wania i manipulowania grafami. Chociaż nie jest możliwe wyróżnienie niewielkiej liczby „typowych” wzorców grafowych struktur danych, dowolną taką strukturę można analizować poprzez określenie jej pozycji na kilku osiach projektowych. Udzielane odpowiedzi dotyczą następujących pytań: ♦ W jaki sposób są przechowywane wierzchołki? ♦ W jaki sposób są reprezentowane krawędzie?

132

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

♦ W jaki sposób są przechowywane krawędzie? ♦ Jakie graf posiada właściwości? ♦ Jakie oddzielne struktury w rzeczywistości reprezentuje „graf’? Poniżej po kolei zbadamy bliżej każde z powyższych pytań.

4.9.1. Przechowywanie wierzchołków Algorytmy przetwarzające grafy wymagają metody dostępu do wszystkich wierzchołków. W przeciwieństwie do list lub drzew, wierzchołki grafu niekoniecznie są połączone krawędziami. Nawet w przypadku, gdy są połączone, cykle (ang. cycles) występujące w strukturze grafu mogą znacznie utrudnić implementację metody systematycznego przeglądu krawędzi. Z tego względu często używana jest zewnętrzna struktura danych w celu przechowywania i przechodzenia po wierzchołkach grafu traktowanych jako zbiór. Dwa najczęściej wykorzystywane podejścia są związane z przechowywaniem wszystkich wierzchołków w tablicy lub łączeniem ich w postaci listy, jak ma to na przy­ kład miejsce w przypadku uniksowego programu tsort (sortowanie topologiczne)6 struct node_str { NODE **ń_prevp; NODE *n_next: NODE **n_arcs;

/* pointer to previous node's n_next /* next node in graph */ /* array of arcs to other nodes */

(...] char n_name[l];

I* name of this node */

}; W powyższym przypadku wierzchołki są połączone ze sobą w ramach listy dwukie­ runkowej przy użyciu pól n_prevp oraz n_next. Zarówno w przypadku reprezentacji listowej, jak i tablicowej, struktura krawędzi jest nakładana na strukturę przechowującą przy użyciu jednej z metod, które zostaną opisane poniżej. W kilku przypadkach trze­ ba faktycznie znaleźć zbiór wierzchołków grafu reprezentowany i przeglądany przy użyciu połączeń krawędzi. Jako punkt początkowy operacji przechodzenia po wszyst­ kich wierzchołkach grafu jest wówczas używany jeden z wierzchołków. Wreszcie, wy­ stępują przypadki, w których jest używane połączenie dwóch metod reprezentacji lub inna struktura danych. Jako przykład przyjrzymy się strukturze grafowej używanej w bibliotece przechwytywania pakietów sieciowych libpcap. Jest ona wykorzystywana przez programy takie jak tcpdump w celu badania pakietów przesyłanych siecią. Pakiety, które mają zostać przechwycone, są określane przy użyciu szeregu bloków, stanowiących wierzchołki naszego grafu. W celu zoptymalizowania specyfikacji przechwytywania pakietów, graf bloków jest przekształcany do postaci struktury drzewiastej: każdy po­ ziom wierzchołków drzewa jest reprezentowany przez listę wierzchołków dołączonych na pozycji tablicy zależnej od poziomu. Każdy blok zawiera również listę krawędzi, które się z nim łączą. Krawędzie określają bloki, które łączą77. struct edge { 1nt 1d: int code: uset edom; 76

77

netbsdsrc/usr.bin/tsort/tsort.c: 88 - 97. netbsdsrc/lib/libpcap/gencode.h: 9 6 - 127.

Rozdział 4. ♦ Struktury danych języka C struct block *succ; struct block *pred: struct edge *next;

133

/* link list of incoming edges for a node */

}: struct block { int id; struct slist *stmts: /* side effect stmts */

[...] struct block *Hnk: /* link field used by optimizer */ uset dom; uset closure; struct edge *in edges:

[...] }: Reprezentatywny przykład postaci struktury danych przedstawiono na rysunku 4.8. W podanym przykładzie kodu bloki są powiązane razem w formie listy przy użyciu pola link, natomiast krawędzie każdego bloku są dołączane do pola in edges. Krawę­ dzie te są połączone w ramach listy za pomocą pola edge next, zaś bloki, które łączą, określają pola suce oraz pred. Każda lista bloków rozpoczyna się od innego elementu tablicy levels. W przypadku takiej reprezentacji, przejście wszystkich wierzchołków grafu można zakodować jako przechodzenie listy w pętli78. struct block **1evels:

[...] 1nt 1; struct block *b:

[...] for (1 - root->level; i >- 0; --i) ( for (b - levels[i]: b; b - b->link) { SET_INSERT(b->dom. b->id):

Rysunek 4.8. Dostęp do wierzchołków grafu poprzez tablicę list

levels

4.9.2. Reprezentacje krawędzi Krawędzie grafu są zwykle reprezentowane albo pośrednio poprzez wskaźniki, albo bezpośrednio jako odrębne struktury. W modelu pośrednim krawędź jest po prostu reprezentowana przez wskaźnik z jednego wierzchołka na drugi. Jest to model używany do reprezentowania wierzchołków tsort, które omówiono w podrozdziale 4.9.1. W każ­ dym wierzchołku grafu tablica n_arcs zawiera wskaźniki na wierzchołki, z którymi bieżący się łączy. Na rysunku 4.9 przedstawiono graf liczący trzy wierzchołki oraz sposób reprezentacji jego krawędzi jako wskaźniki. 78netbsdsrc/lib/libpcap/optimize.c: 150, 255 - 273.

134

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 4.9. G raf o trzech wierzchołkach i jego reprezentacja w programie tsort

Ö

:

W wielu przypadkach krawędzie grafu są reprezentowane bezpośrednio, co umożliwia przechowywanie w nich dodatkowych informacji. Przykładowo, weźmy pod uwagę program gprof, używany do analizowania zachowania programów w czasie uruchomie­ nia (więcej informacji na temat jego działania Czytelnik znajdzie w podrozdziale 10.8). Istotnym elementem takich informacji jest g ra f wywołań (ang. cali graph) programu, opisujący sposób, w jaki funkcje programu wywołują inne funkcje. W programie gprof jest to reprezentowane za pomocą grafu. Jego krawędzie służą do przechowywania in­ 79 formacji o krawędziach powiązanych struct arcstruct { struct nl struct nl long double double struct arcstruct struct arcstruct struct arcstruct unsigned short unsigned short

*arc_parentp; *arc_childp: arc_count: arc_t1me; arc_ch1ldt1me; *arc_parentl1st; *arc_ch1ldlist: *arc_next: arc_cyclecnt: arc_flags;

/* pointer to parent's nl entry */ /* pointer to child's nl entry */ /* num calls from parent to child */ /* time Inherited along arc */ /* chlldtlme Inherited along arc */ /* parents-of-th1s-ch1ld 11st */ /* chlldren-of-thls-parent list */ /* 11st of arcs on cycle */ /* num cycles involved in */ /* see below */

}: Każda krawędź łączy dwa wierzchołki (reprezentowane przez strukturę struct nl) w celu przechowania związku wywołania z wierzchołka-rodzica do wierzchołka-potomka, na przykład funkcja main wywołuje funkcję prin tf. Ten związek jest reprezentowany przez krawędź wskazującą na odpowiednie wierzchołki rodzica (arc_parentp) oraz potomka (arc_childp). Mając daną krawędź, program musi znaleźć inne krawędzie, które prze­ chowują informacje o podobnych związkach dotyczących rodzica (wywołującego) lub potomka (wywoływanego) danego wierzchołka. Łącza do takich krawędzi są przecho­ wywane w postaci list arc_parentlist oraz arc_ ch ild list. W celu zilustrowania takiej struktury możemy zbadać, w jaki sposób będzie reprezentowany niewielki fragment programu. Weźmy po uwagę poniższy fragment kodu8’1. 79

netbsdsrc/usr.bin/gprof/gprofc : 1 1 0 - 121.

80 netbsdsrc/usr.bin/at.

r

Rozdział 4. ♦ Struktury danych języka C

135

int maindnt argc. char **argv)

{ [...] usage():

[...]

fprintf(stderr.

asctime(tm));

[...] exit(EXIT SUCCESS):

} void usageC) (void) fprintfistderr. “tstststs". "Usage: at [-q x] [-f file] [-m] time\n". [...] exitCEXIT FAILURE).

1

m

Graf wywołań dla powyższego kodu przedstawiono na rysunku 4.10. Strzałki oznaczają wywołanie funkcji. Odpowiednią strukturę danych programu gprof przedstawiono na rysunku 4.11. W tym przypadku strzałki są reprezentowane przez pola parent i child rekordów arcstruct. Rekordy są również powiązane w listy oznaczające elementy nad­ rzędne i potomne tych samych wierzchołków-potomków i wierzchołków-rodziców. W celu uproszczenia zagadnienia nie omawiamy wskaźników z wierzchołków na kra­ wędzie. Jeżeli Czytelnik uzna, że taki poziom złożoności jest zbyt duży. może spróbo­ wać prześledzić kilka związków wywołujący-wywoływany oraz jedną z list na przed­ stawionym grafie. Ponadto warto wziąć pod uwagę, że zaprezentowana struktura danych w opinii Autora stanowi graniczny przypadek skomplikowanej struktury, jaką ludzki umysł jest w stanie objąć jako całość. Bardziej skomplikowane struktury powinny być (i zwykle są) rozbijane na odrębne jednostki.

Rysunek 4.10. Prosty g ra f wywołań

Rysunek 4.11. Graf wywołań reprezentowany jako struktura danych programu gprof

136

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

4.9.3. Przechowywanie krawędzi Krawędzie grafu są zwykle przechowywane przy użyciu dwóch różnych metod: jako tablica w ramach struktury każdego wierzchołka (jak miało to miejsce w przykładzie programu tsori z rysunku 4.9) lub jako lista dołączona do każdego wierzchołka grafu. W większości przypadków graf zmienia swoją strukturę w miarę upływu czasu, więc taka tablica jest dynamicznie przydzielana i używana przy użyciu wskaźnika przecho­ wywanego w wierzchołku. Krawędzie grafu przechowywane jako listy dołączone do wierzchołków można zbadać, rozpatrując sposób, w jaki program make przechowuje informacje o zależnościach elementów. Poniżej zbadamy pełną definicję wierzchołka wykorzystywaną przez program make. Każdy wierzchołek (zwykle część konfiguracji konsolidacji programu) zależy od innych wierzchołków (swoich potomków) i jest używany w celu konsolidacji jeszcze innych wierzchołków (swoich rodziców). Są one przechowywane jako listy w ramach struktury wierzchołka81. typedef struct GNode (

[...] Lst 1st

parents: children;

/* Nodes that depend on this one */ /* Nodes on which this one depends */

[...] ) GNode:

Weźmy teraz pod uwagę zależności reprezentowane przez poniższy prosty plik Makefile (stanowiący specyfikację konsolidacji dla programu make) używany do kompilowania programu patch w systemie Windows NT82. 08JS - [...] util.obj version.obj HORS - EXTERN.h INTERN.h [...]

[...] patch.exe: S(OBJS) $(CC) S(OBJS) »(LIBS) -o $@ »(LDFLAGS)

[...] util.obj: t(HDRS) version.obj: »(HDRS)

Zależności reprezentowane przez powyższy plik Makefile (patch.exe zależy od util.I obj, util .obj zależy od EXTERN.h itd.) przedstawiono na rysunku 4.12. Odpowiednia struktura danych generowana przez program make zawiera dla każdego wierzchołka listę jego potomków (version.obj jest potomkiem patch.exe) oraz listę jego rodziców (EXTERN.h oraz INTERN.h są rodzicami version.obj). Oba związki są reprezentowane w strukturze danych. Jak widać na rysunku 4.13, każdy element listy zawiera wskaźnik Gnode, używany do oznaczenia wierzchołka, który pozostaje w określonym związku z danym wierzchołkiem, oraz wskaźnik next, używany do łączenia z innymi krawę­ dziami, które pozostają w tym samyin związku.

81

netbsdsrc/usr.bin/make/make.h: 112- 165.

82 XFree86-3.3/xc/util/patch/Makefile, nt: 15 - 40 .

i

Rozdział 4. ♦ Struktury danych języka C

137

Rysunek 4.12.

Zależności wprogramie reprezentowane w pliku Makefile

Rysunek 4.13.

Zależności wprogramie reprezentowane w strukturze danych programu make

4.9.4. Właściwości grafu Pewne właściwości grafu są istotne nie tylko dlatego, że pozwalają zdać egzamin z przedmiotu „Struktury danych” — pozwalają też poznać sposoby czytania kodu dotyczącego grafów. W grafie skierowanym (ang. directional) krawędzie (w tym przy­ padku często nazywane tukami — ang. arcs) są związane z kierunkiem od jednego wierzchołka do drugiego. Ta właściwość ma wpływ na sposób reprezentacji, dodawa­ nia i przetwarzania elementów grafu. W grafie nieskierowanym (ang. nondirectionat), w którym wierzchołki krawędzi nie są związane z określonym kierunkiem lub atry­ butem, reprezentacja danych powinna traktować pary połączonych wierzchołków jako równoważne (chociażby przechowując krawędzie wiodące w obu kierunkach), a kod przetwarzania podobnie powinien nie wyróżniać żadnego z wierzchołków pod wzglę­ dem kierunku.

A A

83

W grafie spójnym (ang. connected) zawsze istnieje droga z jednego wierzchołka do każ­ dego innego, natomiast w grafie zawierającym cykle (ang. cycles) istnieje więcej niż jedna droga łącząca dwa wierzchołki. Obie powyższe właściwości mają wpływ na sposób przechodzenia po grafie. W przypadku grafów niespójnych kod przechodzenia powinien zostać zapisany tak, aby uwzględniał odizolowane podgrafy. W przypadku grafów zawierających cykle kod przechodzenia powinien być zapisany tak. aby można było uniknąć wpadnięcia w pętlę w trakcie pokonywania cyklu. Unikanie cykli zwykle imple­ mentuje się poprzez oznaczanie wierzchołków odwiedzonych i zapobieganie kolejnym odwiedzinom takich wierzchołków83.

netbsdsrc/usr.bin/tsort/tsort.c: 397 - 403.

138

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

* avoid infinite loops and Ignore portions of the graph krown * to be acyclic

*/ if (from->n_flags & CNF_N00EST|NF_MARK|NF_ACYCL1C)) return (0); from->n_f)ags |- NF_MARK:

4.9.5. Struktury ukryte Wiele tak zwanych „grafowych” struktur danych jest czasem bardziej skomplikowane niż może to sugerować ich nazwa. Ze względu na fakt, że nie istnieje żaden określony abstrakcyjny typ danych lub ustalona metodologia reprezentowania grafów, często poje­ dyncza struktura danych jest używana do reprezentowania wielu różnych elementów j nakładających się na siebie. Oznaką występowania takiej sytuacji jest obsługa wielu struktur przechowywania krawędzi. Ponownie weźmy pod uwagę pełną definicję wierz­ chołka używaną przez program makeM. typedef struct GNode (

[...] Lst Lst Lst 1st Lst [.. } GNode

cohorts: parents: children: successors: preds:

/* /* /* /* /*

Other Nodes Nodes Nodes Nodes

nodes for the :; operator */ that depend on this one */ on which this one depends */ that must be made after this one */ that must be made before thisone */

Logicznym rozwiązaniem jest zapewnienie, aby wierzchołek posiadał listy parentsl i chi 1dren, reprezentujące istniejące zależności w obu kierunkach. Jednak, jak widać, każdy wierzchołek takiego grafu zawiera również listy successors, preds oraz cohorts. j Oczywiście w grafie zależności wierzchołków znajduje się inny graf. który powinien przyciągnąć uwagę uważnego czytelnika kodu.

4.9.6. Inne reprezentacje Oczywiście istnieją również reprezentacje grafów' różne od tych. które omówiliśmy. Na przykład w literaturze informatycznej można znaleźć grafy reprezentowane przy użyciu tablic dwuwymiarowych. Element tablicy A(m, n) reprezentuje krawędź wiodącą | z wierzchołka m do wierzchołka n. Takie reprezentacje rzadko spotyka się w pracują­ cych programach, gdyż trudności związane z dynamicznym zmienianiem rozmiaru tablic dwuwymiarowych sprawiają, że rozwiązanie to pozostawia wiele do życzenia. Ćwiczenie 4.25. Zlokalizuj na płycie CD-ROM dołączonej do książki lub w swoim i środowisku trzy przykłady kodu wykorzystującego struktury grafowe. Wyjaśnij, w jaki sposób są reprezentowane wierzchołki i krawędzie, omów' istotne właściwości grafu oraz narysuj diagram ilustrujący organizację struktury grafu.

84

netbsdsrc/usr.bin/make/make.lr. 112- 165.

Rozdział 4. ♦ Struktury danych języka C

139

Dalsza lektura Klasycznym kompendium dotyczącym materiału omówionego w niniejszym rozdziale jest pozycja Knutha Sztuka programowania, a w szczególności rozdział 2. z tomu 1 [Knu97] poświęcony strukturom przechowywania informacji oraz rozdział 6. z tomu III [Knu98] poświęcony technikom wyszukiwania, takim jak mieszanie. Wydajność omó­ wionych struktur przedstawiono z teoretycznego punktu widzenia w pozycji Aho i in. [AHU74], natomiast konkretne algorytmy zapisane języku C można znaleźć w pozy­ cjach Sedgewicka [Sed97, SedOlj. Należy pamiętać, że teoretyczne przewidywania odnośnie do wydajności stosowania określonych struktur danych nie zawsze sprawdzają się w praktyce. Przykładowo, okazało się, że mieszające struktury danych lub proste drzewa binarne są w praktyce często wydajniejsze od bardziej wyrafinowanych struktur drzew samorównoważących [WZH01]. Ataki przepełnienia bufora stanowią wciąż skuteczny sposób nadużywania systemów. Wczesnym przykładem takiego ataku było opracowanie w 1988 roku robaka Internet Worm [ER89, Spa89]. Opracowano wiele technik obronnych w celu unikania proble­ mów' z przepełnieniem bufora, używanych w czasie kompilacji i działania programów, jak również związanych z wykorzystaniem bibliotek. Jedno z nowszych rozwiązań opi­ sano w pozycji Baratloo i in. [BTSOO]. Zakresy asymetryczne i ich pomoc w unikaniu błędów zliczenia o jeden omówiono wyczerpująco w pozycji Koeniga [Koe88, s. 36 - 46]. Czasem można spotkać statyczne mapy mieszające nie zawierające żadnych kolizji i nie powodujące marnowania przestrzeni pamięciowej. Funkcje mieszające używane do generowania takich map noszą nazwę doskonałych funkcji mieszających (ang. perfect hash functions). Są one czasem tworzone ręcznie przez zapalonych, kreatywnych lub znudzonych programistów. Można je również tworzyć automatycznie, używając pro­ gramu gpe//[Sch90].

Rozdział 5.

Zaawansowane techniki sterowania przebiegiem programów W rozdziale 2. omówiono szereg instrukcji, które mają wpływ na przebieg działania programów. Chociaż opisane instrukcje sterujące wystarczą w przypadku większości zadań programistycznych i należą do spotykanych najczęściej, pewne rzadziej używane elementy są jednak równie istotne w wielu zastosowaniach. Kod rekurencyjny (ang. recursive code) często odzwierciedla struktury danych lub algorytmy o podobnej defi­ nicji. Wyjątki (ang. exceptions) są używane w językach C++ i Java do ułatwienia obsłu­ gi błędów. Wykorzystując równoległość (ang. parallelism) programowy lub sprzętowy można poprawić czas odpowiedzi programów, zdefiniować strukturę przydziału prac lub w wydajny sposób wykorzystywać maszyny wieloprocesorowe. Kiedy mechanizmy równoległe nie są dostępne, programy mogą wykorzystywać sygnały asynchroniczne (ang. asynchrous signals) — sygnały, które mogą być przekazywane w dowolnym mo­ mencie — oraz skoki nielokalne (ang. nonlocal jumps) w celu udzielania odpowiedzi na zewnętrzne zdarzenia. Wreszcie, w celu zwiększenia wydajności, programiści używają niekiedy mechanizmu makropodstawień (ang. macro substitutions) preprocesora języka C zamiast zwykłego mechanizmu wywoływania funkcji.

5.1. Rekurencja Wiele struktur danych (takich jak drzewa i sterty), operacji (takich jak typowanie (ang. type inference) i ujednolicanie (ang. unification)), pojęć matematycznych (takich jak liczby Fibonacciego i grafy fraktalne) oraz algorytmów (takich jak sortowanie szybkie, przechodzenie po drzewach i rekurencyjna zstępująca analiza składniowa) definiuje się w sposób rekurencyjny. Definicja rekurencyjna (ang. recursive definition) pojęcia lub operacji określa dany obiekt w kontekście jego samego. Takie definicje nie są nieskoń­ czone, jak mogłoby się wydawać, ponieważ definicja warunku początkowego (ang. base

142

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

case defmitiori) zwykle definiuje przypadek specjalny, który nie jest zależny od defi­ nicji rekurencyjnej. Przykładowo, silnię z liczby całkowitej n, czyli «!. można zdefi­ niować jako n(n - 1)!, jednak definiuje się również warunek początkowy jako 0! = 1. Algorytmy i struktury danych zdefiniowane rekurencyjnie są często implementowane poprzez użycie definicji funkcji rekurencyjnych. Chociaż wywołania funkcji rekuren­ cyjnej stanowią po prostu przypadek szczególny wywołań funkcji, o jakich była mowa w podrozdziale 2.2, to jednak sposób ich zapisu oraz metody odczytu są wystarczająco odmienne, aby potraktować je specjalnie. Weźmy pod uwagę zadanie analizy składniowej poleceń powłoki systemow-ej. Struktura poleceń obsługiwana przez powłokę Boume’a systemu Unix jest dość skomplikowana. Reprezentatywną część gramatyki poleceń powłoki zilustrowano na rysunku 5.1. Typ polecenia występujący przed dwukropkiem jest definiowany przez jedną z reguł wystę­ pujących w poniższych wierszach. Należy zauważyć, jak wiele poleceń jest definiowa­ nych rekurencyjnie na więcej niż tylko jeden sposób. Polecenie potok może być pole­ ceniem polecenie lub poleceniem potok, po którym występuje symbol potoku (|), po którym występuje polecenie polecenie. Polecenie polecenie może być ujętym w nawia­ sy poleceniem lista-poleceń, które może być poleceniem andor, które z kolei może być [Tl poleceniem potok, a to z kolei znów poleceniem polecenie. Posiadając taką gramatykę, zwykle chcemy rozpoznawać polecenia, przechowywać je w odpowiedniej struktura danych i przetwarzać dane. Rekurencja stanowi kluczowe narzędzie w przypadku prze­ prowadzania tych działań. Rysunek 5.1.

polecenie-proste

Gramatyka poleceń powłoki Bourne a

polecenie:

element element polecenia-prostego polecenle-proste ( hsta-poteceń) { lista-poleceń} for nazwa do lista-poleceń done for nazwa In słowo .. do lista-poleceń done wh11e lista-poleceń do lista-poleceń done u n til lista-poleceń do lista-poleceń done case słowo in część-csse ... esac 1f lista-poleceń then lista-poleceń el se częić-else fi potok: polecenie potok | polecenie andor. potok andor && potok andor 11 potok lista-poleceń: andor lista-poleceń ; lista-poleceń & lista-poleceń ; andor lista-poleceń i andor

|T] Poszczególne polecenia powłoki są przechowywane wewnętrznie jako drzewa. Ponieważ różne typy poleceń mogą składać się z różnych elementów, wszystkie wierzchołki drzewa są reprezentowane przy użyciu struktury union wszystkich możliwych elementów1,2, j 1 netbsdsrc/bin/sli/nodetypes —

używane jako plik wejściowy do automatycznego generow ania

definicji wierzchołków. Warto zwrócić uwagę na użycie tej samej nazwy zarówno jak znacznika struktury, jak i składowej unii.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

143

union nocie {

1nt type: struct nbmary nbinary. struct ncmd ncmd: struct npipe npipe: struct n if n if; struct nfor nfor: struct ncase ncase. struct n c lis t n c lis t.

[...] }: Typ każdego elementu jest rekurencyjnie definiowany jako struktura zawierająca wskaź­ niki na wierzchołki drzewa poleceń odpowiednich dla danego typu. struct n if { 1nt type: union node *test: union node *1fpart. union node *elsepart:

1: Należy zauważyć, że w powyższym kodzie unia i odpowiednia struktura współużytkują |T1 wspólny element in t type. W ten sposób pojedyncze struktury mogą uzyskiwać dostęp do pola type unii używając odpowiedniego pola struktury, noszącego tę samą nazwę. A Nie jest to zbyt dobra praktyka programistyczna, gdyż współzależność położenia pól struktury i unii jest zapewniana przez programistę i nigdy nie jest weryfikowana przez kompilator lub system wykonawczy. Gdyby owo położenie się zmieniło, program wciąż kompilowałby się i uruchamiał, ale mógłby ulegać awarii w nieprzewidywalny sposób. Ponadto, zgodnie z naszą interpretację standardu języka C, istnienie korelacji między dwoma położeniami jest pewne jedynie w przypadku rozwikływania za pomocą wskaź­ nika pierwszego elementu struktury. Na listingu 5.13 przedstawiono kod używany do drukowania poleceń powłoki systemu Unix wykonywanych w tle w odpowiedzi na wewnętrzne polecenie powłoki jobs. Funkcja cmdtxt pobiera tylko jeden argument —wskaźnik na drzewo polecenia, które ma zostać wyświetlone. Pierwsze sprawdzenie wykonywane wewnątrz funkcji cmdtxt (listing 5.1:1) ma podstawowe znaczenie: zapewnia, że polecenie reprezentowane przez wskaź­ nik NULL (na przykład pusta część else polecenia if) zostanie poprawnie obsłużone. Warto zauważyć, że obsługa poleceń pustych nie wykorzystuje rekurencji — funkcja cmdtxt po prostu kończy swoje działanie. Ze względu na fakt, że polecenia są repre­ zentowane jako drzewo, a liście wszystkich drzew (wierzchołki terminalne) mają war­ tość NULL, rekurencyjne przechodzenie gałęzi drzewa pozwala w końcu dotrzeć do liścia i zakończyć działanie. Owo sprawdzenie warunku początkowego (ang. base case test) i jego nierekurencyjna definicja gwarantują, że funkcja rekurencyjna zostanie w końcu zakończona. Nieformalna argumentacja, taka jak przedstawiona powyżej, stanowi istot­ ne narzędzie w podejmowaniu prób zrozumienia kodu wykorzystującego rekurencję. Kiedy zrozumiemy, w jaki sposób może działać funkcja zdefiniowana w kontekście samej siebie, reszta kodu staje się oczywista. W przypadku każdego innego niepustego polecenia (wierzchołka drzewa, listing 5.1:4) jest ono drukowane na podstawie jego części składowych. Przykładowo, lista poleceń rozdzielonych średnikami jest wyświe­ tlana poprzez wydrukowanie pierwszego polecenia z listy (listing 5.1:2). wydrukowa­ nie średnika oraz wydrukowanie drugiego polecenia (listing 5.1:3). ! netbsdsrc/bin/sh/jobs.c: 959 - 1082.

144

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Listing 5.1. R ek u re n cy jn e d ru k o w a n ie d rzew a p o le c e n ia p o d d a n e g o an a lizie składniow ej STATIC void cmdtxttunion node *n)

I

union node *np: struct nodelist *lp.

/* ... */ 1 f tn — NULL) • ------------------

- / / ; Warunek początkowy polecenie puste ■powrót

re tu rn :

switch (n->type) { c a s e NSEMi:

crodtx

t(n -

>nbin a r y . c h i ) :

c m d p u ts l" :

[4] p o le c en ie !; polecenie! [ 2 j Rekurencyjne wydrukowanie pierwszego polecenia

");

c r a d t x t ( n - > n b i n a r y . ch2):• ---¡3] Rekurencyjne wydrukowanie drugiego polecenia b rea k :

~[4] polecenie 1 cii£ polecenie2

c a s e NAND:

cmdtxt(n->nblnary.chl): cndputsC && "): cmdtxt(n->nb1nary.ch2): b reak :

/* .. */ c a s e N PIPE :

for iIp - n->npipe.cmdlist : lp . lp - lp->next) { cmdtxt(lp->n):

~14] Polok poleceń -[5 ] Rekurencyjne wydrukowanie kaidego polecenia

1 f ( lp - > n e x t) o n d p u tst" b rea k :

/* ,

*/

) Rekurencyjna definicja gramatyki, jaką przedstawiono na rysunku 5.1. umożliwia rów­ nież analizę składniową z użyciem szeregu funkcji, które są zgodne ze strukturą grama­ tyki — jest to rekurencyjny zstępujący analizator składniowy (ang. recursive descent \ parser). Takie podejście jest często wykorzystywane w przypadku analizy składniowej względnie prostych struktur, takich jak języki poleceń albo języki dziedzinowe, wyra­ żenia lub tekstowe pliki danych. Na listingu 5.24 przedstawiono reprezentatywne ele­ menty kodu używanego do analizy składniowej poleceń powłoki. Warto zauważyć symetrię występującą między kodem analizy składniowej a gramatyką. Rozszerza się ona nawet na nazewnictwo identyfikatorów, choć przedstawiony analizator został napisany przez Kennetha Almquista co najmniej dziesięć lat po tym. jak Stephen Boume zaim­ plementował oryginalną powłokę i udokumentował jej gramatykę. Struktura rekuren-1 cyjnego zstępującego analizatora składniowego jest bardzo prosta. Każda funkcja jest odpowiedzialna za analizę określonego elementu gramatyki (listy poleceń, polecenia I andor, potoku lub polecenia prostego) oraz za zwrócenie wierzchołka składającego się z tego elementu. Wszystkie funkcje działają poprzez wywołanie innych funkcji w celu | dokonania analizy składniowej elementów gramatyki, które tworzą dane polecenie, oraz funkcji readtokenO, służącej do odczytywania zwykłych znaczników, takich jak if lub while. netbsdsrc/bin/sli/parser.c: 157-513.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

145

Listing 5 .2 . Rekurencyjny zstępujący analizator składniowy dla poleceń powłoki STATIC union node * U s t O n t nlflag) { union node *nl. *n2. *n3:

- Analiza składniowa listy poleceń

if (nlflag — 0 && tokendlistlpeektokenO]) return NULL; nl - NULL; for (::) ( n2 - andorOi#-----------------------

-L ista zawiera polecenia sndor

[...] STATIC union node * andort) ( union node *nl. *n2. *n3; Int t: nl - pipelined:•-------for (;;) { 1f CCt - readtokenO) t - NAND; ) else If (t — TOR) { t - NOR: ) else { tokpushback++: return nl;

-A naliza składniowa polecenia andor

- [ ! ] Polecenie andor składa się z potoków

TAND) {

L [...]

-HI

STATIC union node * pipelined { union node *nl. *pipenode: struct nodelist *lp, *prev:

- Analiza składniowa potoku

nl - coramandO 1f (readtokend — TPIPE) { plpenode - (union node *)stalloc(sizeof (struct nplpe)); p1penode->type - NP1PE: plpenode->npipe.backgnd * 0: lp - (struct nodellst *)stalloc(sizeof (struct nodelist)): pipenode->npipe.cmdlist - lp; ____________________________________ 1p->n - nl: prev * lp; Ip - (struct nodelist *)stal1oc(sizeof (struct nodelist)). lp-»n - commandD prev->next - lp; ) while (readtokend — TPIPE); lp->next - NULL; nl - pipenode:

- [2] Potok składa się z poleceń

■Połączenie poleceń w lisię

-(2.1

}

return nl;

} STATIC union node * conmandO { switch (readtokend) { case TIF nl - (union node *)stalloc(sizeof (struct nif)): nl.-nif test - llst(O).*if (readtokenO !- ITHEN) synexpect(TTHENi:

______

" Analiza składniowa polecenia

- Analiza składniowa poleceń i f

- [3] Każda część polecenia jest listą - Spodziewany znacznik then; w przeciwnym razie zgłoszenie błędu składniowego

146

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source ni--»if.ifpart - ItsUfl).»------n2 - nl: [...] 1f (lasttofcen — TELSE) n2->nif.elsepart - Tist(O):• — else ( n2->nif elsepart - NULE: tokpushback++: 1 [...]

----- U l

,

----- 13]

Funkcje, które drukują polecenia z wierzchołków i poddają je analizie składniowej, nie I są ściśle rekurencyjne, gdyż żadne polecenie występujące w ich treści nie wywołuje I bezpośrednio funkcji, która je zawiera. Jednak funkcje te wywołują inne, które z kolei I wywołują jeszcze inne, a te wreszcie wywołują oryginalne funkcje. Tego rodzaju reku-1 rencję określa się mianem rekurencji wzajemnej (ang. mutual recursion). Chociaż taki I kod może wydawać się bardziej skomplikowany niż kod rekurencji prostej, można go I z łatwością analizować biorąc pod uwagę rekurcncyjną definicję odpowiednich pojęć, I na których go oparto. Kiedy wszystkie rekurencyjne wywołania funkcji występują tuż przed miejscem wyj- I ścia z funkcji, mówi się, że funkcja jest rekurencyjna ogonowo (ang. tail recursive). I Rekurencja ogonowa stanowi ważne pojęcie, ponieważ kompilator może optymali-® zować takie wywołania w postaci pojedynczych skoków, co pozwala zaoszczędzić na I czasie wywołania funkcji oraz zmniejszyć wymagany narzut pamięciowy. Wywołania I rekurencji ogonowej są równoważne powrotowi pętli do wykonywania funkcji od po-1 |T| czątku. Czasem można spotkać wywołania rekurencji ogonowej używane zamiast zwy-1 kłych pętli, instrukcji goto lub continue. Kod z listingu 5.35 stanowi tu odpowiedni I przykład. Jego celem jest zlokalizowanie serwera (nazywanego w kodzie źródłowym I driver) dla wieloużytkownikowej gry hunt. Jeżeli nie zostanie znaleziony żaden serwer,® kod podejmuje próbę uruchomienia nowego, czeka na jego zarejestrowanie i ponownie I wykonuje procedurę lokalizacji serwera wykorzystując rekurencję ogonową. L isting 5 .3 . R ekurencja o g onow a użyta zamiast pętli

M

void find_dr1ver(FLAG do_startup)

0) • ------------------------------------- - Ten kod jest wykonywany, jeśli nie wystąpi wyjątek lOException 1og([.. ]);

Wyjątki w języku Java są obiektami generowanymi przez podklasy klasy java.lang. Throwable, która posiada dwie podklasy standardowe: java.lang.Error (zwykle uży­ wana do wskazania błędów nienaprawialnych) oraz java.lang.Exception (używana do określenia warunków, które można przechwycić i naprawić). Można znaleźć wiele przypadków, w których definiuje się nowe klasy wyjątków jako podklasy powyższej8. public final class Llfecycle Exception extends Exception (

Wyjątki są generowane albo bezpośrednio w odpowiedzi na wystąpienie błędu9 public void addCh1ld(Contamer chlld) {[...] 1f (chlldren.get(child.getNameO) !- nuli i throw new HlegalArguroentException( "addChUd: Chlld name + chlld.getNameO +

ts not unique"):

albo pośrednio w rezultacie wywołania metody, która wywołuje wyjątek. W obu przy­ padkach, jeżeli wygenerowane wyrażenie nie jest podklasą (zawsze generowanych) wyjątków Error lub RuntimeException, metoda musi deklarować wyjątek poprzez klau­ zulę throws (listing 5.4:1). Przykładem wyjątku generowanego pośrednio jest IOException z listingu 5.4. Można z łatwością zlokalizować metody, które mogą powodować pośrednio generowane wyjątki poprzez uruchomienie kompilatora języka Java względem kodu źródłowego klasy po usunięciu klauzuli throws z definicji metody. Jeżeli wyjątek nie jest przechwytywany w ramach metody, w której może wystąpić, następuje jego propagacja w górę stosu wywołań metod do podprogramu wywołującego, następnie do podprogramu wywołującego ten ostatni itd., dopóki nie zostanie przechwy­ cony. W kodzie z listingu 5.5 wyjątek LifeCyc'eException, który pojawi się w metodzie validatePackages10, nie jest przechwytywany w przypadku wywołania przez metodę start", ale w końcu zostaje przechwycony, gdy metoda start zostanie wywołana przez g

jt4/catalina/src/share/org/apache/catalina/LifecycleExceptionjava\ 77. jt4/catalina/src/share/org/apache/catalina/core/Co>uamerBasejava: 775 - 781.

wjl4/catalina/src/share/org/apache/catalina/ioader/StandardLoader.java\ 1273 - 1268. " jt4/catalina/src/share/org/apache/cataUna/loader/StandardLoaderjava: 583 - 666.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

149

metodę ContainerBase setLoader12. Nieprzechwycone wyjątki powodują zakończenie

A programu z komunikatem diagnostycznym oraz wyświetleniem stosu śledzenia. Takie zachowanie może być mylące dla użytkownika, więc należy zapewnić, aby programy sprawdzały i odpowiednio obsługiwały wszystkie wyjątki. Listing 5 .5 . Obsługa błędów w języku Java public final class StandardLoader [...]{[ ] private void validatePackagesO throws LifecycleException {

[-..]

if (¡found) • -- Wystąpienie wyjątku throw new LifecycleException ("Hissing optional package " + required[1]):

[ 1 } [...] public void startO throws LifecycleException {

[...] validatePackagesO: • -------------------

[...] ) [. .]

Wyjątek może zostać wygenerowany, ale nie zostanie przechwycony

1 public abstract class ContainerBase [...]{[ .] public synchronized void setLoaderfLoader loader) { [...] if (started && (loader !- null) && (loader Instanceof Lifecycle)) { try {--------------------------------------- • ---- Przechwycenie wyjątku ((Lifecycle) loader).startO: } catch (LifecycleException e) ( log("ContainerBase.setLoader: start: ". e)

1 } [...]

1

1 Czasem można spotkać puste klauzule catch13. try (

[...] } catch (MalformedURLException e) { } catch (IOException e) {

1 |T| Puste klauzule catch są używane w celu zignorowania wyjątków, zwykle dlatego, że nie

są one istotne dla wykonywanego kodu, co przedstawia poniższy fragment kodu14. ) catch (NamingExceptlon e) { // Silent catch: it's valid that no /WEB-INF/1ib directory // exists

} Można również spotkać klauzule final ly używane bez odpowiadających im klauzul HI catch. W większości przypadków służą one do określania pewnych działań, które mają zostać wykonane po zakończeniu bloku try, kiedy zostanie osiągnięty koniec kodu, n jt4/catalina/src/share/org/apache/catalina/core/ContainerBasejava: 345 - 378. 13jt4/catalina/src/share/org/apache/catalina/loader/StandardLoaderjava: 1028 - 1044.

iAjt4/catalina/src/share/org/apache/catalina/startup/ContextConftgjava: 1065 - 1068.

150

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

instrukcja return lub opatrzona etykietą instrukcja break bądź continue. Przykładowo, w poniższym kodzie wywołanie funkcji reset (mark) jest zawsze wykonywane — nawet w przypadku, gdy przebieg sterowania osiągnie instrukcję return values15. try { 1f ( nextCharO -- *>’ ) return values: ) finally { reset(mark):

1 Powyższy kod można więc odczytać jako: if ( nextCharO — ' >'){ reset(mark); return values;

)

reset(mark);

Ponadto często spotyka się klauzulę finally używaną do czyszczenia przydzielonych zasobów, takich jak deskryptory plików, połączenia, blokady i obiekty tymczasowe. W poniższym fragmencie kodu połączenie Connection conn zostaje zamknięte bez względu na to, co stanie się w trakcie wykonywania 28 wierszy kodu bloku try16. conn - getConnectionO: try {

[...] ) catch ( SQlExceptlon e ) { transformer.get TheLoggerO.errort "Caught a SQLException", e ): throw e: } finally { conn.closeO: conn - nul 1:

1 Obsługa błędów w języku C++jest bardzo podobna, choć istnieją pewne różnice. ♦ Obiekty wyjątków mogą być dowolnego typu. ♦ Brak obsługi klauzuli finally. ♦ Destruktory obiektów lokalnych są wywoływane w momencie zwolnienia stosu. ♦ Funkcje deklarują obsługiwane wyjątki poprzez użycie deklaratora throw zamiast występującej w języku Java klauzuli throws. Ćwiczenie 5.5. Zlokalizuj sekwencję wywołań funkcji języku C++ lub Java, która sygna­ lizuje błędy poprzez wyjątki, i przepisz ją, nie wykorzystując wyjątków, a modyfikując j wartości zwracane przez funkcję. Porównaj czytelność obu rozwiązań. Ćwiczenie 5.6. Skompiluj metodę w kodzie Java po usunięciu z niej klauzuli throws w celu znalezienia metod generujących występujące wyjątki. Powtórz tę procedurę dla metod powodujących wyjątki, rysując drzewo propagacji wyrażeń.

i5jl4/jasper/src/share/org/apache/jasper/compiIer/JspReaderjava: 606 -611. 16cocoon/src/java/org/apache/coeoon/lramformation/SQLTransformer.java: 978 - 1015.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

151

Ćwiczenie 5.7. Kod obsługi napędu taśmowego programu archiwizującego w systemie BSD1 zawiera wiele przypadków, gdzie wywołania systemowe są bezpośrednio po­ równywane ze zwróconą wartością-1 , która oznacza niepowodzenie. Opracuj w zarysie sposób przepisania tego kodu w języku C++ z użyciem wyjątków.

5.3. Równoległość Niektóre programy wykonują część swojego kodu równolegle w celu zmniejszenia czasu odpowiedzi, ustrukturyzowania przydziału prac lub wydajnego wykorzystania wielu komputerów albo komputerów posiadających wiele procesorów. Projektowanie i im­ plementacja takich programów wciąż stanowi obszar prowadzonych badań — stąd też można spotkać wiele różnych modeli abstrakcji i programistycznych. W niniejszym podrozdziale zostaną omówione pewne reprezentatywne przykłady często spotykanych modeli dystrybucji pracy, a pominięte pewne bardziej egzotyczne implementacje, takie jak wykorzystujące równoległość strojoną lub obliczenia wektorowe. Możliwość rów­ noległego wykonywania operacji może dotyczyć warstwy sprzętowej lub programowej.

5.3.1. Równoległość sprzętowa i programowa Poniżej opisano pewne typy równoległości, które można spotkać na poziome sprzętowym.

Działanie wielu jed n o stek wykonawczych w jednym procesorze O ile nie pracuje się nad kompilatorem lub procesorem, z tym typem równoległości spotyka się jedynie czytając lub pisząc kod symboliczny dla architektur procesorów, co przenosi ciężar synchronizacji instrukcji na programistę.

Inteligentne zintegrow ane lub zew nętrzne urządzenia Współczesne urządzenia, takie jak napędy taśmowe, karty graficzne i sieciowe, modemy oraz drukarki posiadają własne procesory i wykonują polecenia wysokopoziomowe (na przykład: „zapisz ścieżkę na dysku”, „narysuj wielokąt") równolegle z głównym pro­ cesorem komputera. Kod związany z tymi urządzeniami jest odizolowany przez system operacyjny oraz sterowniki urządzeń.

W sparcie sprzętow e dla wielozadaniowości Większość współczesnych procesorów obsługuje wiele mechanizmów, takich jak prze­ rwania i sprzętowa obsługa pamięci, które pozwalają systemom operacyjnym szerego­ wać wiele zadań w sposób dający wrażenie, jakby były one wykonywane równolegle. Kod implementujący taką funkcjonalność stanowi część systemu operacyjnego, zaś dwie często występujące abstrakcje programistyczne, o których będzie mowa, to proces (ang. process) oraz wątek (ang. thread). Są one wykorzystywane do zapewnienia enkapsulacji zadań wykonywanych równolegle. 17netbsdsrc/sbin/restore/tape. c.

152

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Maszyny w ieloprocesorow e Takie maszyny stają się coraz powszechniejsze, stąd implementacje programistyczne coraz częściej wykorzystują dostępną moc obliczeniową. Tu także procesy i wątki są zwykle wykorzystywane w celu wspomożenia systemu operacyjnego w rozłożeniu pracy fTl pomiędzy procesory. Używając tych samych abstrakcji w środowiskach wielozadanio­ wych z pojedynczymi procesorami i w wieloprocesorowych, otrzymuje się kod. który jest przenośny między dwiema różnymi klasami architektur sprzętowych.

M odele rozproszone Komputery połączone w sieci są coraz częściej używane równolegle w celu rozwią­ zywania problemów wymagających znacznych zasobów obliczeniowych lub w celu zwiększenia wydajności czy niezawodności. Do typowych zastosowań należą oblicze­ nia naukowe (takie jak rozkład na liczby pierwsze dużych wartości lub poszukiwanie form życia pozaziemskiego) oraz działania scentralizowanej infrastruktury e-biznesu w oparciu o wykorzystanie wielu serwerów. Kod pisany dla takich systemów bazuje na istniejących klastrowych systemach operacyjnych, na oprogramowaniu pośrednim,! na aplikacjach, lub jest pisany od podstaw z wykorzystaniem elementarnych technik sieciowych. Poniżej opisano modele używane na poziomie oprogramowania do reprezentowania! kodu, który jest wykonywany lub wydaje się być wykonywany równolegle.

Proces Proces to abstrakcja wykorzystywana przez system operacyjny w celu reprezentacji! pojedynczej instancji wykonywanego programu. Systemy operacyjne używają dostęp-l nego sprzętu w celu przełączania się między procesami, co pozwala na jego wydajniej-1 sze wykorzystanie, daje mechanizm budowania złożonych systemów oraz elastycznej i szybko reagujące środowisko. Tworzenie, kończenie i przełączanie się między pro-1 cesami to względnie kosztowne operacje. Dlatego też zwykle procesom przydziela się I do wykonania dość obszerne prace. Komunikacja i synchronizacja działania procesów! są obsługiwane przez różne mechanizmy oferowane przez system operacyjny, na przy-1 kład obiekty pamięci współdzielonej, semafory i potoki.

W ątek Pojedynczy proces może składać się z wielu wątków: są to równolegle wykonywani! ciągi operacji. Wątki mogą być implementowane na poziomie użytkownika lub stano-1 wić rdzenny mechanizm oferowany przez system operacyjny. Wszystkie wątki procesu! współużytkują wiele zasobów, z których największą rolę odgrywa pamięć globalnaJ Stąd tworzenie, kończenie i przełączanie się między wątkami nie są operacjami zbyt! kosztownymi i wątkom często przydziela się do wykonania mało obszerne zadanii! Komunikacja między wątkami odbywa się zwykle poprzez globalną pamięć proces« z użyciem elementarnych obiektów synchronizujących, takich jak funkcje wzajemnie! wykluczające, które są wykorzystywane do zapewnienia spójności działań.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

153

Modele doraźne Z różnych realnych lub teoretycznych względów mających związek z dostępnością obu abstrakcji, które opisano powyżej, możliwościami konserwacji, wydajnością lub po prostu ignorancją, w niektórych programach implementuje się swoiste mechanizmy obsługi równoległości (rzeczywistej lub wirtualnej) pracy. Takie programy są uzależ­ nione od odpowiednich mechanizmów elementarnych — przerwań, sygnałów asyn­ chronicznych. odpytywania, skoków nielokalnych oraz zależnych od architektury ope­ racji stosowych. Niektóre z tych mechanizmów zostaną omówione w podrozdziałach 5.4 oraz 5.5. Typowym przykładem takiego kodu jest jądro systemu operacyjnego, które często zapewnia odpowiednią obsługę procesów i wątków. Wiele systemów operacyjnych rozkłada procesy i wątki pomiędzy wiele procesorów. Z tego powodu struktura kodu wykorzystującego maszyny wieloprocesorowe jest często projektowana z uwzględnieniem właściw ości procesów i wątków'.

5.3.2. Modele sterowania P~1 Okazuje się, że kod wykorzystujący mechanizmy równoległości pracy jest zwykle związany z trzema różnymi modelami: 1. Model zespołowy (ang. work crew model), w którym zestaw podobnych zadań działa równolegle. 2. Model zwierzchnika-podwladnego (ang. boss/worker model), w którym zadanie nadrzędne rozdziela pracę między podrzędne. 3. Model potokowy (ang. pipeline model), w którym seria zadań operuje na danych i przekazuje je do kolejnego zadania. Każdy z modeli zostanie poniżej opisany bardziej szczegółowo. Model zespołowy jest używany w celu równoległego wykonywania podobnych operacji poprzez przydzielenie ich do wielu zadań. Każde zadanie wykonuje ten sam kod i czę­ sto jedyna różnica polega na przydzielonej pracy. Zespołowy model równoległy jest stosowany w celu rozłożenia pracy pomiędzy procesory lub utworzenia zestawu zadań używanych do przydzielania standardowych części wykonywanej pracy. Przykładowo, wiele implementacji serwerów WWW przydziela zespołowi procesów lub wątków przy­ chodzące żądania HTTP. Na listingu 5.6 przedstawiono typowy przykład takiego kodu18. Program rysuje kształty obramowania w różnych oknach. Każde okno jest obsługiwane przez odrębny wątek, który wykonuje funkcję do_ico_window. Wszystkie wątki współ­ użytkują ten sam obszar pamięci globalnej, więc kod tworzenia wątków przydziela obszar pamięci closure w celu przechowywania prywatnych informacji wątku. Chociaż można spotkać wiele interfejsów zarządzania wątkami, większość z nich uruchamia je w sposób podobny do przedstawionego w przykładzie: funkcja uruchomienia wątku określa funkcję, która ma zostać wykonana oraz parametr, który zostanie przekazany do 1SXFreeS6-3.3/contrib/programs/ico/ico.c: 227 - 369.

154

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

|T] instancji funkcji określonego wątku. Parametr ten zwykle wskazuje na obszar pamięci używany do przechowywania danych dotyczących wątku lub do przekazywania infor­ macji swoistych dla utworzonego wątku. Listing 5.6. Kod wielowątkowy modelu zespołowego ____________________________________________________ maindnt argc. char **argv)

{

[...] /* start all but one here */ for (1-1; 1 0) { next * U s t : 0 ----------------------------------------------------------- [ l] Moie wskazywać na element. list ■ thi S: który zostanie usunięty

} 40 socket/Fork.C: 43 - 159.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów void Fork::ForkProcess::reaper_nohang (1nt signo)

163

Funkcja obsługi sygnału

{ [••■!

ForkProcess* prev - 0: ForkProcess* cur - list: while (cur) {_______ ____ if (cur->pld ~ w p l f T p ---------------cur->pid - -1: 1f (prev) prev->next - cur->next: else 11st - Hst->next: • -----------------

[...] delete cur; • ------------------------break:

Czy to zakończony proces ?

[2] Może usunąć element, na który wskazuje nowowstawiona wartość [3] Może powodować konflikt wykonania z innymi operacjami w obszarze pamięci

1

prev - cur: cur - cur->next;

} [...] }

A

m

W tym miejscu pojawia się problem. Funkcja obsługi sygnału przegląda listę w celu zlokalizowania zakończonego procesu poprzez jego identyfikator wpid. Po znalezie­ niu procesu zostaje on usunięty z listy, a pamięć, która została dla niego dynamicznie przydzielona, jest zwalniana przy użyciu operacji delete. Jednakże ze względu na fakt, że funkcja obsługi sygnału jest wywoływana asynchronicznie, może zostać uruchomio­ na tuż po wykonaniu instrukcji z listingu 5.15:1. Jeżeli zakończony proces jest pierw­ szym na liście, instrukcja z listingu 5.15:2 podejmie próbę usunięcia procesu z listy, jednak ze względu na fakt, że na zakończony proces już wskazuje nowo wstawiony element, ostatecznie otrzymuje się listę zawierającą szczegóły dotyczące procesu, który już wyczyszczono, przechowywane w obszarze pamięci, który zostanie zwolniony. Drugi wyścig może być powodowany przez operację delete (listing 5.15:3). Jeżeli funkcja obsługi sygnału, a w konsekwencji operacja delete, zostanie uruchomiona w czasie, gdy inna część programu wykonuje operację new lub inną operację delete, mogą one zostać pomieszane, co spowoduje powstanie błędów w obszarze przydzielo­ nej dynamicznie pamięci programu. Wielowątkowa wersja biblioteki języka C++ może zawierać zabezpieczenia przeciwko wyścigom podobnym do drugiej z opisanych sytu­ acji, jednak w aplikacji nie zastosowano ich, gdyż program nie korzystał bezpośrednio z wątków. W rezultacie aplikacja działała bez zarzutu tworząc tysiące procesów i nie­ spodziewanie kończyła działanie, kiedy obciążenie systemu przekraczało pewien określo­ ny poziom. Ze względu na fakt, że awaria była powodowana błędem w pamięci, miej­ sce jej wystąpienia nie miało nic wspólnego z powodującym ją problemem wyścigu. Wniosek płynący z powyższego przykładu jest taki, że należy z ogromną ostrożnością badać kod manipulujący strukturami danych i wywołania biblioteczne, które występują w funkcji obsługi sygnału. W szczególności warto zauważyć, że standard ANSI C okre­ śla, że niejawnie wywoływane funkcje obsługi sygnałów mogą wywoływać jedynie funkcje abort, exit, longjmp oraz signal i mogą przypisywać wartości jedynie do obiek­ tów statycznych zadeklarowanych jako volatile sig_atomic_t. Wynik działania jakiej­ kolwiek innej operacji jest niezdefiniowany. Często używanym podejściem do kwestii obsługi sygnałów w poprawny sposób jest ustawienie flagi w odpowiedzi na sygnał badany w odpowiednim kontekście. Kod z listingu 5.1641 demonstruje takie podejście oraz pewne dodatkowe pojęcia związane

41 netbsdsrc/distrib/utils/more.

164

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

z obsługą sygnałów. Funkcja ini t_si gnał s wywołana w celu aktywowania obsługi sygnałów instaluje funkcję obsługi winch odpowiadającą na sygnał SIGWINCH (powia­ damia on aplikację o tym, że zostały zmienione wymiary jej wyświetlanego okna). Kod zawarty w funkcji obsługi sygnału jest bardzo prosty: ponownie aktywuje obsługę sygna­ łów (obsługa sygnału powraca do domyślnego zachowania po otrzymaniu sygnału) oraz ustawia flagę sigs zgodnie z otrzymanym sygnałem. W każdym powtórzeniu głównej pętli programu jest sprawdzana flaga sigs i w razie potrzeby wykonywane wymagane operacje (listing 5.16:3). Interesujący jest także kod dezaktywujący funkcje obsługi sygnałów: obsługa sygnału przerwania SIGINT jest przywracana do zdefiniowanego w ramach implementacji zachowania domyślnego (listing 5.16:1), natomiast sygnały dotyczące zmiany rozmiaru okna są po prostu ignorowane (listing 5.16:2). Listing 5.16. Synchroniczna obsługa sygnałów • - Ustawienie obsługi sygnałów

1n1t_signals(1nt on)

( 1f (on) {

[.,.] (V0ld)s1gnal (SIGWINCH. Winch):• ---- Instalacja funkcji obsługi sygnału

) else {

[...] (VOld)Signal (SIGINT. SIG_DFL);* (V0 ld)si gnat (SIGWINCH. SIG IGN);• —

[IJ Domyślna akcja dla sygnału [2] Ignorowanie sygnału

} } [...] V0i d

• ------------Funkcja obsługi sygnału

winch()

_

{

(yold)signal (SIGWINCH. winch): • -------- Przywrócenie zachowania sygnału sigs I” S WINCH;#------------------------1 stawienie flagi na przetwarzanie synchroniczne

[...] 1 [...] coranandsO

f

[...3 for (;:) {



1Pętla główna

Ćwiczenie 5.13. Zlokalizuj trzy niebanalne funkcje obsługi sygnałów i przeanalizuj ich działanie. Ćwiczenie 5.14. Zbadaj, w jaki sposób wątki współpracują z funkcjami obsługi sygnałów w Twoim środowisku. Ćwiczenie 5.15. Standard ANSI C nie gwarantuje, że funkcje biblioteczne będą współ­ bieżne. Zbadaj funkcje obsługi sygnałów z płyty dołączonej do książki w przypadku wywołań funkcji niewspółbieżnych.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

165

5.5. Skoki nielokalne Funkcje biblioteczne języka C setjmp oraz longjmp pozwalają programom na wykonywa­ nie skoków powrotnych z funkcji bez konieczności wykonania instrukcji return w każdej z kolejnych funkcji. Są one zwykle wykorzystywane do natychmiastowego wychodze­ nia z głęboko zagnieżdżonych funkcji, często w odpowiedzi na pewien sygnał. Wyko­ rzystuje się je razem. W punkcie, w którym nastąpi powrót z zagnieżdżonych funkcji, używa się setjmp w celu przechowania w zmiennej buforowej kontekstu środowiska. Od tego miejsca aż do punktu wyjścia z funkcji, która wywołała setjmp, wykonanie funkcji longjmp ze wskaźnikiem na zapisane informacje o środowisku jako argumentem powoduje przeniesienie sterowania do punktu, w którym oryginalnie wywołano setjmp. W celu odróżnienia oryginalnego wywołania setjmp od późniejszych powrotów do tego miejsca poprzez funkcję longjmp, ta pierwsza zwraca wartość 0 przy wywołaniu ory­ ginalnym oraz wartość niezerową przy późniejszych. Przykład z listingu 5.1T42 stanowi typowy przypadek. Użytkownicy edytora tekstowego ed mogą zatrzymywać długie operacje, powracając do wiersza poleceń po wywołaniu [T| przerwania ('“C). Osiąga się to, instalując funkcję obsługi sygnału dla przerwania kla­ wiaturowego i wywołując setjmp przed pętlą przetwarzania poleceń. Oryginalne wy­ wołanie setjmp zwraca wartość 0 i program wykonuje normalne przetwarzanie wstępne (listing 5.17:2). Kiedy użytkownik naciśnie kombinację *C (na przykład w celu przerwania wyświetlania długiego listingu), funkcja obsługi sygnału signal_int wy­ wołuje funkcję handle_int, która powoduje wykonanie skoku powrotnego na po­ czątek pętli poleceń (listing 5.17:1). Listing 5.17. Skok nielokalny przy użyciufunkcji longjmp 1nt malrKint argc, char *argv[])

{

[...] signal(SIGINT, s1gnal_1nt): • ---1f ((status - setjmp(env)) !- 0) { fputs('\n?\n". stderr); spr1ntf(errmsg. "interrupt'): ) else { init buffers!): • -----------

■Ustawienie funkcji obsługi sygnału [1] Wywołanie 1ongjmp powoduje przekazanie sterowania tutaj

[2] Po wywołaniu funkcji

setjmpsterowanie powraca tutaj

[...] )

for (::) {

"

•—

Pętla przetw arzania poleceń

[...] 1f (prompt) { printfCtS". prompt): fflush(stdout);

) [...] void signal_int(int slgno)

{

• ------------

[...] handle_int(slgno);

} 42 netbsdsrc/bin/ed/main.c: 114 - 1417.

Funkcja obsługi sygnału wywołująca funkcję handle_int

166

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source void . ----------bandle_1nt(int Signo) ------------------------------Wywoływana przez funkcji; obsługi sygnału

(

[...] 1ongjmp(env. *1); • -----------------------------Przeskok z powrotem na początek pętli poleceń

1 Choć na pierwszy rzut oka kod wydaje się wolny od błędów, może wystąpić wiele kom-

A plikacji. Sygnał pojawiający się w złym momencie w połączeniu z wywołaniem

longjmp

w celu przeskoczenia na początek pętli poleceń, może spowodować wyciekanie zasobów (przykładowo, zostawiając otwarty plik lub nie zwalniając tymczasowo przydzielonego bloku pamięci) lub, co gorsza, pozostawienie struktur danych w niespójnym stanie. Zbadany przez nas kod wykorzystuje sekcje wzajemnego wykluczania w celu unik­ nięcia tego problemu. Fragment z listingu 5.1843 pokazuje, w jaki sposób można to osiągnąć. Definiowane są dwa makra, SPL1 oraz SPLO, służące do wchodzenia i wycho­ dzenia z sekcji krytycznych. (Nazwy makr pochodzą od funkcji używanych do usta­ wiania poziomów priorytetów we wczesnych wersjach jądra systemu Unix). SPL1 usta­ wia zmienną mutex. Funkcje obsługi sygnałów sprawdzają wartość tej zmiennej: jeżeli została ustawiona, zamiast wywoływać funkcję handl e_i nt, po prostu zachowują numer sygnału w zmiennej sigfłags. Makro SPLO — wywoływane na końcu sekcji krytycznej — zmniejsza wartość zmiennej mutex, sprawdza czy zmienna sigfłags nie zawiera jakichś oczekujących sygnałów, odebranych w trakcie wykonywania sekcji krytycznej i w razie potrzeby wywołuje funkcję handl e int. Korzystając z takiego schematu może zapewnić, aby odpowiedzi na sygnały były po prostu opóźniane do momentu wyjścia sterowania z sekcji krytycznej. Listing

5.18. U staw ienie se k c ji w zajem nego w ykluczania w celu och ro n y stru k tu r danych int mutex - 0: int sigfłags - 0;

/* if set. signals set "sigfłags" */ /* if set. signals received while mutexset */

W e fine SPLIO mutex-*-* • — Wejście do sekcji krytycznej; opóźnienie obsługi sygnału • — Wyjście z sekcji krytycznej

#define SPLOO \ if (--mutex —

0) { \

if (sigfłags if (Sigfłags

&(l «

(SIGHłJP - 1))) handle_hup(SIGHUP): \ * ------ Obshiga zawieszonych signalów (SIGINT - 1))) handleJnt(SIGINT): \

&(1 «

} VOi d signa1_tnt(int signo)

• — Funkcja obsługi sygnału

( if (mutex)

Sigfłags | * (1 «

(signo -! ) ) : • -------W sekcji wzajemnego wykluczania; do opóźnienia obsługi

else

wystarczy ustawić tę jłagę Wzajemne wykluczanie nie ustawione — natychmiastowa obsługa przerwania

handl e_1nt( Signo): * } •

VOld

Natychmiastowa lub opóźniona obsługa sygnału przerwania

handlejnt(int signo)

{

[;,•] sigfłags

-(1 «

1Ongjmp (en v . -1 ); •

43

netbsdsrc/bin/ed.

(Signo -1 ) ) : # ------------Wyzerowanie fla g i przerwania Skok powrotny do pętli poleceń

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

167

void c1ear_active_listO

i

SPL1(); • -------------------------------------------------- Wejście do sekcji krytycznej active size - active last - active ptr - active ndx - 0: • — Sekcja krytyczna freetactive 11st): activejist - NULL: SPLOO; • -------------------------------------------------- Wyjście z sekcji krvtvcznej

) Ćwiczenie 5.16. Wyjaśnij, w jaki sposób na listingu 5.18 definicje makr SPLO oraz SPL1 umożliwiają zagnieżdżone działanie sekcji krytycznych. Ćwiczenie 5.17. Zlokalizuj pięć wywołań funkcji longjmp i wymień powody ich użycia. Dodatkowo spróbuj zlokalizować wywołania longjmp nie mające związku z sygnałami. Ćwiczenie 5.18. W jaki sposób zlokalizowany kod używający longjmp pozwala uniknąć wyciekania zasobów i niespójności struktur danych? Ćwiczenie 5.19. Zaproponuj sposób weryfikowania, czy funkcja longjmp nie jest wy­ woływana w sytuacji, gdy funkcja zawierająca setjmp zakończyła już działanie. Czy będzie to metoda czasu kompilacji czy uruchomienia?

5.6. Podstawienie makr Elastyczność preprocesora języka C jest często wykorzystywana w celu definiowania prostych funkcji w postaci makrodefinicji4'*. #define IS IOENT(c) (1salnum(c) |1 (c) — ' ' |1 (c) — #define IS_0CTAL(c) (- '0' && (c) n) ASSERTtprocesstn) — else ASSERT(processtk) —

0): 0);

A Po rozwinięciu makra otrzymany kod będzie miał postać: if (k > n) 1f eflags®_TRACE) printft"—Xs\n". (str)): }

Jednak użycie makra NOTE w kontekście: If (k > 1) NOTECk > D : else process(k):

A po rozwinięciu daje kod: if (k > 1) { if (m->ef1ags®_TRACE) printft”-*s\n*. C k > 1")): }; else process(k):

który nie będzie mógł zostać skompilowany z uwagi na występowanie niepotrzebnego średnika przed słowem kluczowym else. Z tego powodu w makrodefinicjach często spotyka się sekwencje instrukcji wewnątrz bloku do { ...} while (0)47. Mefine getvndxfer(vnx) do ( \ int s - splbiot): \ (vnx) - (struct vndxfer *)get_pooled_resource(&vrtdxfer_head): \ splx(s): \ } while (0)

45 netbsdsrc/lib/libc/stdlib/malloc.c: 130. 46 nelhsdsrc/Iib/libc/regex/engine.c: 128. 47 netbsdsrc/xys/vm/vm_swap.c: 293 - 297.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

169

HI Bloki, oprócz tworzenia zakresu definiowania zmiennych lokalnych, są również ujmo­ wane w ramach instrukcji d o .. .while w celu zapewnienia ochrony instrukcji if przed niechcianymi interakcjami. Przykładowo, poniższy kod4inext) I--------- ¡1] GETNEXTO (*jP>next++) ■--------- /// MOREO (p^>next < ^>end) |-- ¡ 1]

static void________________ 9 ___ p strtregister struct parse *p) 1-------- ¡2]

f

REQUIRE (MORE O. REGJMPTY): while (MOREO) ordinarytp. GETNEXTO):

1 [...] static int /* the value */ p_count(register struct parse *p) 1-------- /3]

{ 48 49

netbsdsrc/sys/vm /vm _sw ap.c: 101 - 104. netbsdsrc/lib/libc/regex/regcom p.c: 1 5 3 - 154.

50 netbsdsrc/Iib/libc/regex/regcom p.c: 150 - 677.

170

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

register int count - 0: register int ndlgits ■ 0; while ( M OREO && isdigit(PEEKO) && count tf_vm86_##reg - (u_short) vrr86s.regs.vmsc.sc_##reg #define DOREG(reg) tf->tf_##reg - (u_short) vm86s.regs.vmsc.sc_##reg

D0VREG(ds): D0VREG(es): 0OVREG(fs): D0VREG(gs): OOREG(edi): DOREG(esi): DOREG(ebp):

zostanie rozwinięty do postaci: tf->tf_vm86_ds - (u_short) vnt86 s .regs.vmsc.sc_ds; tf->tf_vm86_es * (u_short) vm86s.regs.vmsc.sc_es: tf->tf_vm86_fs * (u_short) vm86 s.regs.vmsc.sc_fs; tf->tf_vm86_gs - (u_short) vm86 s.regs.vmsc.sc_gs: tf->tf_edi - (u_short) vm86s .regs.vmsc.sc_edi: tf->tf_es1 - (u_short) vm86s.regs.vmsc.sc_es1 : tf->tf_ebp - (u_short) vm86 s.regs.vmsc.sc_ebp;

Daje to dynamicznie tworzony kod zapewniający dostęp do pól struktury o nazwach takich jak tf_vm86_ds lub sc_ds.

51 netbsdsrc/bin/rcp/rcp.c: 595 - 606. 52 netbsdsrc/sys/arch/i386/i386/vm86.c: 419 - 428.

Rozdział 5. ♦ Zaawansowane techniki sterowania przebiegiem programów

171

|T | Starsze kompilatory (sprzed wprowadzenia standardu ANSI C) nie obsługiwały ope­ ratora ##. Jednak można było je zmusić do złączenia dwóch znaczników poprzez roz­ dzielenie ich pustym komentarzem (/**/). Taki zapis można spotkać czytając starszy kod. W poniższym fragmencie kodu wyraźnie widać, w jaki sposób działanie operatora ## można było symulować w starszych programach5’. fifdef _ S T D C _ #def1ne CAT(a.b) #else Idefine CAT(a.b)

a##b a/**/b

fendif Ćwiczenie 5.20. Zlokalizuj wszystkie definicje makr min oraz max w kodzie źródłowym, zliczając te. które zdefiniowano w poprawny sposób oraz te. które dają w wyniku błęd­ ny kod. Ćwiczenie 5.21. Niektóre kompilatory języka C++ i C obsługują słowo kluczowe inline, służące do definiowania funkcji, które są bezpośrednio kompilowane w momencie ich wywołania, co pozwala uniknąć narzutu wynikającego z wywołania skojarzonej funkcji i daje kompilatorowi dodatkowe możliwości optymalizacji w kontekście podprogramu wywołującego. Porównaj ten mechanizm z makrodefinicjami pod względem czytelno­ ści, możliwości konserwacji oraz elastyczności kodu wykorzystującego każdą z dwóch metod. Ćwiczenie 5.22. Porównaj czytelność kodu języka C++ opartego na szablonach (pod­ rozdział 9.3.4) z podobnymi mechanizmami zaimplementowanymi za pomocą makrodefinicji. Utwórz, korzystając z obu podejść, prosty przykład i porównaj komunikaty o błędach kompilatora po wprowadzeniu do kodu błędów składniowych i znaczeniowych.

Dalsza lektura Pełną gramatykę poleceń powłoki systemu Unix, o której mowa w podrozdziale 5.1, można znaleźć w pozycji Boume’a [Bou79], Więcej informacji na temat rekurencyjnej zstępującej analizy składniowej można znaleźć w pozycji Aho i in. [ASU85, s. 44 - 55]. Prace poświęcone architekturze komputerów oraz systemom operacyjnym oferują wiele materiałów dotyczących sposobów obsługi i udostępniania równoległości na różnych poziomach sprzętowych i programowych [Tan97, HP96, PPI97, BST89]. W szczególno­ ści: wątki omówiono w dwóch innych źródłach [NBF96, LB97], zaś działanie sygnałów i wątków w pozycji Robbinsów [RR96], Wiele przykładów programowania wykorzy­ stującego potoki można znaleźć w pozycji Kemighana i Pike’a [KP84], natomiast teo­ retyczne podstawy używania potoków zawarto w pozycji Hoarego [Hoa95]. Projekto­ wanie systemów operacyjnych daje wiele możliwości poznania kwestii związanych ze współbieźnością. Zostały one omówione w kilku źródłach [Bac86, Tan97, LMKQ88. MBK96]. Często pojawiające się problemy dotyczące makrodefinicji omawia praca Koeniga [Koe88], zaś korzyści i wady związane z użyciem funkcji rozwijanych w miej­ scu wywołania wyczerpująco opisuje Meyers [Mey98],

53netbsdsrc/sys/arch/bebox/include/bus.h: 97 - 103.

Rozdział 6.

Metody analizy dużych projektów Duże, wieloplikowe projekty różnią się od mniejszych nie tylko wyzwaniami stojącymi przed czytającymi ich kod, ale również pod względem oferowanych przez nie możli­ wości lepszego zrozumienia kodu. W niniejszym rozdziale zostaną omówione pewne techniki często stosowane w implementowaniu dużych projektów, a następnie zostaną zbadane określone elementy, które zwykle składają się na proces opracowywania takich systemów. Zostaną opisane sposoby organizacji dużych projektów, związane z nimi procesy budowy i konfiguracji, metody sprawowania kontroli nad różnymi wersjami plików, specjalna rola narzędzi projektowych oraz typowe strategie testowania. Zostaną tu również przedstawione ogólne charakterystyki tych elementów oraz pewne wskazówki dotyczące sposobów ich użycia w celu zwiększenia możliwości przeglądania i analizo­ wania kodu.

6.1. Techniki projektowe i implementacyjne Niebagatelny wysiłek programistyczny związany z dużymi systemami, wynikający z ich rozmiaru i zakresu, często uzasadnia wykorzystanie technik, które w innych sytuacjach nie byłyby odpowiednie. Wiele takich technik stanowi przedmiot opisu w różnych czę­ ściach niniejszej książki. Jednak warto w tym momencie przedstawić w zarysie pewne często spotykane metody projektowe i implementacyjne oraz zawrzeć odnośniki do odpowiednich rozdziałów i podrozdziałów.

Wizualne przetw arzanie program istyczne i techniki formalizacji W przypadku dużych projektów, elementy cyklu życia oprogramowania, które pośred­ nio podlegają rozbiciu na mniejsze przedsięwzięcia, stanowią część bazy kodu oprogra­ mowania. Ponadto nieunikniona złożoność i duża liczba programistów biorących udział

174

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

w niebanalnych projektach wymuszają przyjęcie pewnych formalnych zasad działania. Określają one zwykle, w jaki sposób ma być używany dany język, determinują sposób organizacji elementów projektu, przedmiot dokumentacji oraz procesy związane z więk­ szością aktywności dotyczącej cyklu życia projektu. W rozdziale 7. zostaną przedsta­ wione standardy pisania kodu. proces budowy projektu stanowi temat podrozdziału 6.3, procedury testowe — podrozdziału 6.7, metody konfiguracji — podrozdziału 6.4, kontrola wersji — podrozdziału 6.5, zaś w rozdziale 8. zostaną omówione techniki tworzenia dokumentacji.

Niebanalna arch itek tu ra W przypadku niedużych projektów można łączyć ze sobą bloki kodu do momentu speł­ nienia stawianych wymagań. Jednak wysiłki związane z opracowaniem dużego systemu wymagają utworzenia struktury budowanego systemu poprzez użycie odpowiedniej architektury, pozwalającej na przezwyciężenie problemów związanych z jego złożo­ nością. Architektura zwykle dyktuje strukturę systemu, sposób sterowania nim oraz modularny rozkład na poszczególne elementy. W przypadku dużych systemów często wykorzystuje się również pewne koncepcje architektoniczne poprzez schematy, wzorce projektowe oraz architektury dedykowane. Wszystkie te kwestie zostaną omówione w rozdziale 9.

Dekompozycja Na poziomie implementacyjnym w przypadku dużych projektów można stwierdzić występowanie daleko idącej dekompozycji poszczególnych elementów poprzez użycie mechanizmów takich jak funkcje, obiekty, abstrakcyjne typy danych oraz komponenty. Kwestie te zostaną omówione w rozdziale 9.

Obsługa wielu platform Duże aplikacje często przyciągają zainteresowanie sporej rzeszy użytkowników. W re­ zultacie takie aplikacje zwykle muszą uwzględniać wiele kwestii związanych z prze­ nośnością. które w przypadku mniej ambitnych projektów są po prostu pomijane.

Techniki obiektow e Złożoność dużych systemów można często pokonać, używając technik projektowania oraz implementacji obiektowych. Obiekty organizuje się zwykle w strukturach hierar­ chicznych. Dziedziczenie (omawiane w podrozdziale 9.1.3) jest wykorzystywane do określenia wspólnych zachowań wielu obiektów, natomiast techniki rozdzielania (oma­ wiane w podrozdziale 9.3) umożliwiają przetwarzanie kolekcji różnych obiektów przez pojedynczy blok kodu.

Przeciążanie operatorów Duże fragmenty kodu pisanego w językach takich jak C++ i Haskell wykorzystują | przeciążanie operatorów w celu definiowania związanych z projektem typów danych jako równoważnych typom wbudowanym, a następnie manipulowania nimi przy uży-1 ciu wewnętrznego zbioru operatorów. Adekwatne przykłady można znaleźć w pod­ rozdziale 9.3.3.

Rozdział 6. ♦ Metody analizy dużych projektów

175

Biblioteki, kom ponenty i procesy Na wyższym poziomie szczegółowości kod dużych systemów jest często rozkładany na biblioteki modułów obiektów, odrębnych procesów oraz komponentów umożliwiających wielokrotne użycie. Techniki te zostaną omówione w podrozdziale 9.3.

Języki i narzędzia dedykow ane i niestandardow e Duży wysiłek programistyczny często wiąże się z tworzeniem specjalizowanych narzę­ dzi lub wykorzystywaniem już istniejących, stosowanych w podobnych środowiskach. Więcej informacji na temat sposobów użycia specjalizowanych narzędzi zawarto w podrozdziale 6.6.

Masowe wykorzystanie przetw arzania w stępnego Projekty implementowane w asemblerze, C i C++ często wykorzystują preprocesor w celu rozszerzenia języka o struktury dedykowane. Współczesne projekty rzadko ko­ duje się w językach symbolicznych. Wykorzystanie preprocesora języka C omówiono w podrozdziale 5.6. Ćwiczenie 6.1. Zaproponuj sposoby szybkiego określania, czy dany projekt jest zgodny z którąś z opisanych technik projektowych lub implementacyjnych. Przetestuj swoją propozycję względem jednego z głównych projektów zawartych na płycie dołączonej do książki. Ćwiczenie 6.2. Zlokalizuj zalecane praktyki projektowe i implementacyjne w pod­ ręczniku z zakresu inżynierii oprogramowania. Wyjaśnij, w jaki sposób odzwierciedla je kod źródłowy danego projektu.

6.2. Organizacja projektu Organizację projektu można zbadać przeglądając jego drzewo kodu źródłowego — hierarchiczną strukturę katalogów, zawierających kod źródłowy projektu. Drzewo kodu źródłowego często odzwierciedla architekturę projektu oraz strukturę procesu progra­ mistycznego. Weźmy pod uwagę drzewo kodu źródłowego serwera WWW apache f il (rysunek 6.1). Na najwyższym poziomie przypomina ono typowy serwer WWW: wy­ stępują tu katalogi służące do przechowywania skryptów serwerowych (cgi -bin), plików konfiguracyjnych (conf), treści statycznej (htdocs), ikon oraz dzienników serwera. Drze­ wo kodu źródłowego aplikacji często odzwierciedla strukturę jej wdrożenia. Faktyczny kod źródłowy programu jest przechowywany w katalogu o często używanej nazwie src. Inne katalogi zawierają pliki przykładowe oraz szablony służące do konfigurowania wdrażanej aplikacji. Ponadto w przypadku projektu apache, jego dokumentacja jest przechowywana w postaci statycznych stron WWW w katalogu htdocs/manuał. Kod źródłowy programu również jest zorganizowany w formie struktury katalogów o typo­ wych nazwach: katalog lib zawiera kod bibliotek, main zawiera główny kod serwera, include — pliki nagłów kowe, moduł es — kod opcjonalnych elementów instalacji, zaś

176

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 6.1. Struktura drzewa kodu źródłowego serwera WWW apache

OS — kod specyficzny dla systemu operacyjnego. Niektóre z tych katalogów zawierają dalsze podkatalogi. Katalog 1ib dzieli się na odrębne podkatalogi dla kodu analizatora składniowego XML (e x p a t-lite ) oraz biblioteki indeksowanej bazy danych (sdbm). Katalog os również dzieli się na różne podkatalogi — po jednym dla każdego systemu operacyjnego lub typu platformy. Często stosowaną strategią opracowywania aplikacji wieloplatformowych jest izolowanie kodu związanego z odrębnymi platformami i dosto­ sowywanie go do konkretnych wymagań. Taki kod można później umieścić w odręb­ nych katalogach, z których jeden jest konfigurowany jako uwzględniany w procesie budowy w zależności od używanej platformy. W tabeli 6.1 przedstawiono często wyko­ rzystywane nazwy katalogów w różnych projektach. W zależności od rozmiaru systemu, opisywane katalogi mogą być wspólne dla całego systemu lub każda z jego części może posiadać własną odrębną strukturę katalogów. Wybór między obiema strategiami jest uzależniony od sposobu organizacji procesu opracowywania i konserwowania systemu. Procesy scentralizowane zwykle sugerują użycie wspólnych katalogów, co pozwala na wykorzystanie zbieżnych elementów różnych aplikacji. Z kolei rozproszone procesy projektowe wykorzystują duplikację struktury w każdym z odrębnych projektów, przez co osiąga się niezależność w zakresie ich utrzymywania. W miarę, jak wzrastają rozmiary projektu, jego struktura katalogów jest dostosowywana do określonych wymagań. Historia projektu często ma wpływ na strukturę jego katalo­ gów, gdyż programiści i osoby odpowiedzialne za utrzymywanie systemu podchodzą

Rozdział 6. ♦ Metody analizy dużych projektów

177

Tabela 6.1. Często stosowane nazwy podkatalogów projektów Katalog

Zawartość

sre

Kod źródłowy. Katalog główny kodu źródłowego Java.

TLD1(np. org lub edu) main nazwa-programu lib 1i bnazwa

Kod źródłowy głównego programu, niezależny od platformy, bez bibliotek. Kod źródłowy programu nazwa-programu. Kod źródłowy bibliotek. Kod źródłowy biblioteki nazwa.

common include

Elementy kodu współużytkowane przez aplikacje. Pliki nagłówkowe C i C++.

doc man

Dokumentacja. Strony podręcznikowe pomocy systemu Unix. Microsoft Windows (bitmapy, pliki zasobów, ikony) w formie kodu i (lub) skompilowanej. Kod specyficzny dla architektury danego procesora.

rc / res arch os CVS/ RCS / SCCS build / compile/classes / obj / Release /Debug tools test conf etc eg bin contrib

Kod specyficzny dla danego systemu operacyjnego. Pliki systemu kontroli wersji. Katalog kompilacji. Narzędzia używane w trakcie procesu budowy aplikacji. Skrypty testowe oraz pliki wejściowe i wyjściowe. Informacje konfiguracyjne dla procesu budowy aplikacji. Informacje konfiguracyjne czasu uruchomienia. Przykłady. Pliki wykonywalne i skrypty powłoki. Narzędzia i kod dostarczone przez użytkowników; zwykle utrzymywane poza głównym projektem.

z niechęcią do prób zmiany struktury bazy kodu, do której się przyzwyczaili. Wystarczy porównać strukturę katalogów jądra systemu NetBSD (rysunek 6.2) ze strukturą jądra systemu Linux (rysunek 6.3). Ta ostatnia jest bardziej jednolita po względem podziału na podkatalogi. W przypadku systemu NetBSD wiele katalogów, które można by zgru­ pować razem w ramach jednego katalogu, występuje na najwyższym poziomie struk­ tury tylko ze względów historycznych. Typowym przykładem jest organizacja kodu obsługi sieci. Różne protokoły sieciowe (TCP/IP2, ISO3, ATM4, AppleTalk5) w przy­ padku jądra systemu NetBSD występują jako katalogi najwyższego poziomu, natomiast 1 TLD (ang. top-level domain), przedrostek najwyższego poziomu nazwy domeny —przyp. tłum. 2

netbsdsrc/sys/netinet.

3 netbsdsrc/sys/netiso. 4 netbsdsrc/sys/netnatm. 5 netbsdsrc/sys/netatalk.

178

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 6.2.

Drzewo kodu źródłowego jądra systemu NetBSD arch (separate diagram | m68k4k

cd9660 libkem

freebsd

komfs deadfs miscfs miscfs

kernfs notccitt

neliso

union

w Rozdział 6. ♦ Metody analizy dużych projektów

179

180

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

w przypadku systemu Linux znajdują się one we wspólnym katalogu net. Choć pro­ jektanci kodu obsługi sieci systemu NetBSD w czasie jego opracowywania rozważali możliwość użycia schematu nazewnictwa katalogów, który pozwalałby na rozszerze­ nie i uwzględnianie innych protokołów, to jednak nie zdecydowali się na utworzenie oddzielnego katalogu w celu przechowywania kodu (w owym czasie) tylko jednego protokołu sieciowego. Niekiedy dobrze zdefiniowany proces tworzenia nowego kodu daje w wyniku bardzo uporządkowaną strukturę katalogów. Weźmy pod uwagę część kodu jądra systemu NetBSD związaną z określonymi architekturami procesorów. Przedstawiono ją na ry­ sunku 6.4. Przez lata system NetBSD był przenoszony na dziesiątki różnych architektur. Fragmenty kodu związane z tymi zagadnieniami są zgrupowane w odrębnym katalogu6 i są odpowiednio łączone z głównym drzewem kodu źródłowego w czasie procesów konfigurowania i budowy systemu. Nie należy bać się dużych zbiorów kodu źródłowego — zwykle są one lepiej zorgani­ zowane niż ma to miejsce w przypadku mniejszych, doraźnych projektów. Na rysun­ ku 6.5 przedstawiono zarys całości kodu źródłowego systemu FreeBSD, w tym jądra, bibliotek, dokumentacji, narzędzi użytkowych oraz systemowych. Pomimo jego roz­ miarów (zawiera niemal 3000 katalogów, nie wliczając w to repozytoriów plików kon­ figuracyjnych) zlokalizowanie kodu źródłowego dla określonego narzędzia nie nastrę|~n cza żadnych problemów: kod źródłowy narzędzia znajduje się w katalogu o nazwie zawierającej nazwę narzędzia dołączoną do nazwy odpowiadającej jego ścieżce dostę­ pu. Przykładowo, kod źródłowy narzędzia make (zainstalowanego jako /usr/bin/make) można znaleźć w katalogu /usr/src/usr.bin/make. Pracując nad dużym projektem pierwszy raz, należy poświęcić nieco czasu na zapoznanie się ze strukturą jego drzewa katalogów. Wiele osób preferuje użycie w tym celu narzędzi graficznych, takich jak Eksplorator Windows lub linuksowy midnight commander . Jeżeli żadne z nich nie jest dostępne, strukturę katalogów można badać graficznie, korzystając z przeglądarki in­ ternetowej, w której otwarto lokalny katalog źródłowy. W przypadku większości sys­ temów uniksowych można również użyć polecenia 1ocate w celu znalezienia lokali­ zacji poszukiwanego pliku, zaś w systemie Windows w tym samym celu można użyć mechanizmu wyszukiwania Eksploratora. Przeglądając duży projekt należy pamiętać, że jego „kod źródłowy” to o wiele więcej niż tylko instrukcje języka komputerowego, które są kompilowane w celu otrzymania programu wykonywalnego. Drzewo kodu źródłowego projektu zwykle zawiera również specyfikacje, dokumentację użytkową i systemową, skrypty testowe, zasoby multime­ dialne, narzędzia służące do przeprowadzania procesu budowy elementów systemu, przykłady, pliki lokalizacyjne, pliki kontroli wersji, procedury instalacyjne oraz infor­ macje licencyjne. Ćwiczenie 6.3. Opisz strukturę katalogów wykorzystywaną na płycie dołączonej do książki lub w projektach tworzonych w Twoim przedsiębiorstwie.

6 netbsdsrc/sys/arch. hllp://www. ibiblio. org/mc/.

Rozdział 6. ♦ Metody analizy dużych projektów

Rysunek 6.4.

Drzewo kodu źródłowego częścijądra systemu NetBSD związanej z kwestiami dotyczącymi odrębnych architektur

181

182

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 6.5. Drzewo kodu źródłowego systemu FreeBSD

Rozdział 6. ♦ Metody analizy dużych projektów

183

Ćwiczenie 6.4. W jaki sposób standardowa struktura katalogów może być używana do automatyzacji pewnych aspektów procesu opracowywania oprogramowania? Ćwiczenie 6.5. Zbadaj i opisz strukturę katalogów zainstalowanej wersji systemu Micro­ soft Windows. Ćwiczenie 6.6. Określ, jakie elementy, oprócz kodu źródłowego, są dostarczane w pakie­ cie dystrybucji języka Perl, która jest dostępna na płycie dołączonej do książki.

6.3. Proces budowy i pliki M akefile Większość dużych systemów wykorzystuje wyrafinowany proces budowy. Taki proces zwykle jest związany z obsługą opcji konfiguracyjnych, wieloma typami plików wej­ ściowych i wyjściowych, złożonymi zależnościami wewnętrznymi oraz kilkoma doce­ lowymi platformami. Ze względu na fakt. że proces budowy w efekcie wpływa na otrzymywany produkt wyjściowy, istotną rzeczą jest umiejętność „czytania” procesu budowy projektu, tak jak jego kodu. Niestety, nie istnieje standardowy sposób okre­ ślania i wykonywania tego procesu. Każdy duży projekt i platforma programistyczna wykorzystują odrębne rozwiązania. Jednak pewne elementy są wspólne dla większości z nich. Zostaną one omówione poniżej. Na rysunku 6.6 przedstawiono typowe etapy procesu budowy. Pierwszy z nich to kon­ figuracja opcji oprogramowania oraz ścisłe określenie budowanego przedmiotu (kon­ figuracja stanowi temat podrozdziału 6.4). W oparciu o konfigurację projektu można utworzyć g ra f zależności (ang. dependency graph). Duże projekty zazwyczaj zawierają dziesiątki lub wręcz tysiące różnych komponentów — wiele z nich zależy od innych. Graf zależności określa poprawną kolejność, w jakiej powinny zostać zbudowane różne komponenty projektu. Często niektóre części procesu budowy nie są wykonywane po­ przez wykorzystanie standardowych narzędzi, takich jak kompilator lub konsolidator. lecz wymagają użycia narzędzi, które opracowano specjalnie dla danego projektu. Typo­ wym przykładem jest tu program imake, używany do zapewnienia standaryzacji procesu budowy systemu X Window. Po zbudowaniu narzędzi związanych z projektem mogą one (wraz z innymi narzędziami standardowymi) być użyte do wstępnego przetworzenia, skompilowania i skonsolidowania plików wykonywalnych projektu. Równolegle doku­ mentacja projektu jest często konwertowana z formatu „źródłowego” do końcowego formatu dystrybucyjnego. Może się to wiązać z kompilacją plików pomocy systemu Windows lub złożeniem stron podręcznika systemu Unix. W końcu wynikowe pliki wykonywalne i dokumentacja zostają zainstalowane w systemie docelowym lub przy­ gotowane do wdrożenia lub dystrybucji na dużą skalę. Typowe metody dystrybucji to pliki RPM systemu Red Hat Linux, format instalatora systemu Windows lub pobieranie odpowiednich plików ze strony WWW. Rysunek 6.6. Etapy

f .--------------

.-------

I

Budow» X I------ ---------- -

184

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Zdecydowanie najbardziej skomplikowaną część procesu budowy stanowi definiowanie i zarządzanie zależnościami występującymi w projekcie, Określają one, w jaki sposób różne części projektu są od siebie zależne, a stąd, w jakiej kolejności poszczególne ele­ menty należy zbudować. Na rysunku 6.7 przedstawiono typowy zbiór zależności pro­ jektu. Pliki dystrybucyjne projektu są zależne od istnienia plików wykonywalnych i dokumentacji. Niekiedy ładowane dynamicznie biblioteki związane z projektem oraz komponenty programowe również stanowią część dystrybucji. Pliki wykonywalne zależą od plików obiektów, bibliotek oraz komponentów, gdyż są one wszystkie kon­ solidowane w celu utworzenia tych pierwszych. Biblioteki i komponenty również są zależne od plików obiektów. Z kolei pliki obiektów są zależne od kompilacji odpo­ wiednich plików źródłowych oraz, w przypadku języków takich jak C i C++, od plików nagłówkowych, które zostały określone w plikach źródłowych. Wreszcie, niektóre pliki źródłowe są tworzone automatycznie z plików specjalnych (na przykład plik określający gramatykę zdefiniowaną za pomocą narzędzia yacc jest często używany w celu utwo­ rzenia analizatora składniowego napisanego w C). Stąd pliki źródłowe zależą od kodu dedykowanego oraz odpowiednich narzędzi. W przypadku naszego opisu warto zwrócić uwagę, w jaki sposób wzajemnie się ze sobą wiążą zależności i proces budowy. Sposób wykorzystania tej relacji w celu przeprowadzenia całego procesu zostanie przedstawiony po opisie grafu zależności. Rysunek 6.7.

Typowy zbiór zależności projektu

Na rysunku 6.8 przedstawiono konkretny przykład zależności projektu. Jest to niewiel­ ka. ale adekwatna część związków występujących w przypadku procesu budowy ser­ wera WWW apache. Proces instalacji jest zależny od procesu demona serwera apache — httpd. Plik wykonywalny demona httpd jest zależny od wielu plików obiektów (wśród nich buildmark .o oraz moduł es. o) i bibliotek, takich jak biblioteka XML o na­ zwie libexpat.a oraz libap.a. Biblioteka analizatora składniowego XML jest zależna od wielu różnych plików obiektów. Jak pokazano na rysunku, jeden z nich (xml tok.0) jest zależny od odpowiedniego pliku źródłowego xml to k . c. W przypadku dużych projektów zależności występują między tysiącami plików źródło­ wych i mogą się zmieniać zgodnie ze specyficzną konfiguracją. Przykładowo, wersja serwera apache przeznaczona dla systemu Windows nie zależy od plików źródłowych

Rozdział 6. ♦ Metody analizy dużych projektów

185

Rysunek 6.8.

Adekwatny podzbiór zależności występujących w przypadku serwera WWW apache

V xaltok.c związanych z systemem Unix. Projekty napisane w języku C i C++ jeszcze bardziej komplikują istniejące zależności, ponieważ każdy plik obiektu zależy nie tylko od od­ powiednich plików źródłowych C i C++, ale również od wszystkich plików nagłów­ kowych zawartych w pliku źródłowym. Tok przechodzenia od zależności do konkret­ nego procesu budowy nie jest banalny. Jest związany z wykonaniem topologicznego sortowania na grafie zależności. Wynikiem tego sortowania jest określenie porządku, w jakim mają być konstruowane obiekty procesu budowy (na przykład kompilowanie plików obiektów używanych w bibliotece przed jej skonsolidowaniem). Na szczęście proces ten zwykle podlega automatyzacji dzięki użyciu specjalistycznych narzędzi, takich jak make, nmake i ant. Korzystając z narzędzia podobnego do make, zależności i reguły konstruowania plików z ich elementów składowych określa się w specjalnym pliku, zwykle noszącym nazwę makeftle lub Makefile. Większość etapów procesu budowy zostaje zautomatyzowana poprzez uruchomienie programu make z plikiem makefile jako (często domyślnym) argumentem. Program make odczytuje i przetwarza plik makefile, a następnie wykonuje odpowiednie polecenia służące do przeprowadzenia budowy. Plik makefile zwykle składa się z elementów określających program docelowy, jego zależności oraz reguły generowania programu docelowego na podstawie tych zależności. Przykładowo, poniż­ sze wiersze zawierają informację, że program docelowy patch. exe zależy od pewnych plików obiektów ($(0BJS)) oraz określają regułę wywoływania kompilatora języka Cs. patch.exe: S(OBJS) t(CC) t(OBJS) S(LIBS) -o 10 KLOFLAGS)

W celu zwiększenia możliwości ekspresji plików makefile używa się wielu mechani­ zmów. Niektóre z nich można znaleźć w pliku makefile serwera apache, przedstawio­ nym na listingu 6.1. Należy pamiętać, że pliki makefile dużych projektów są często generowane dynamicznie po przeprowadzeniu etapu konfiguracji — w celu zbadania pliku makefile należy wykonać działania konfiguracyjne związane z projektem.

s XFree86-3.3/xc/util/patch/Makefile.nt: 23

24 .

186

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Listing 6.1. Plik makefile serwera WWW apache OBJS- modules.0 t(MOOULES) \

• ----------------------------[ I j Definicja zmiennej

main/Hbmain.a »(OSD1R)/I1bos.a \ ap/libap.a

[...] . SUFFIXES: def • ------------------------------------------ [2] N ony przyrostek pliku .def.a : • -------------------------------------- f i ] Reguła generowania pliku a emximp -o S9 $< z p lik u .d e /

[...] target_static: subdlrs modules.O • ----------------------------------------------¡4] Pseudoobiekt docelowy zależności $(CC) -C S( INCLUDES) S(CFLAGS) bulldmark.c • Reguhbudawy

»(CC) J(CFLAGS) »(LDFLAGS) I(LDFLAGS_SHLIB_EXPORT) \ -o »(TARGET) bulldmark.o »(OBJS) »(REGL1B) t(EXPATLIB) J(LIBS)

[...] Cl ean j • -------------------------------------------------------------------------------------------------------- [4] Pseudoohiekt docelowy

-rm -f »(TARGET) HbJ(TARGET) .* * 0 • ---------------------------- Reguła budowy

[■■■]............._

_

_

_

_

»(OBJS): Makefile subdirs • ----------------------------------------- [5] Zależność jaw na # 00 NOT REMOVE bulldmark.o: bulldmark.c include/ap_conf1g.hinclude/apjimn.h \ 1nclude/ap_conf1g_auto.h os/unlx/os.h include/ap ctype.h \

• ------ [6] Zależności generowane

automatycznie

[. .] Często definiuje się również zmienne (określane również jako makra) — używając składni ZMIENNA = tekst — w celu określenia często używanych list plików oraz opcji narzędzi. Definicja z listingu 6.1:1 definiuje również zmienną OBJS, która zawiera pliki obiektów, od których bezpośrednio zależy program wykonywalny serwera apache. [Tl Zmienne są zwykle zapisywane wielkimi literami, a dodatkowo często stosuje się pew­ ne standardowe nazwy. Są one używane w plikach makefile poprzez użycie składni $L (dla jednoliterowych nazw zmiennych) lub $(W4ZW4) dla nazw wieloliterowych. W tabeli 6.2 przedstawiono pewne często używane zmienne definiowane przez użytkownika. Tabela 6.2. Zmienne użytkownika często definiowane w plikach makefile Zmienna

Zawartość

SRCS

Pliki źródłowe.

INCLUDES OBJS

Pliki dołączane (nagłówkowe). Pliki obiektów.

LIBS

Biblioteki.

CC

Kompilator języka C.

CCP CFLAGS

Preprocesor języka C. Przełączniki kompilatora języka C.

LFLAGS

Przełączniki konsol idatora.

INSTALL

Program instalacyjny.

SHELL

Powłoka poleceń. W celu uniknięcia powtórzeń często występujących reguł, program make zapewnia możliwość zdefiniowania reguły tworzenia pliku docelowego na podstawie przyrost­ ków plików uwzględnianych w procesie transformacji. Reguła z listingu 6.1:3 określa, w jaki sposób należy generować bibliotekę importu (.a) systemu OS/2 z jej definicji

Rozdział 6. ♦ Metody analizy dużych projektów

187

(.def) poprzez wywołanie polecenia emximp. Większość implementacji narzędzia make zarządza obszernym wewnętrznym zbiorem reguł transformacji oraz dostępnych przy­ rostków nazw plików. Zwykle określają one, w jaki sposób należy kompilować pliki zapisane w języku C, C++, asemblerze lub Fortran, w jaki sposób konwertować pliki yacc i lex do postaci plików języka C oraz w jaki sposób wyodrębniać pliki źródłowe z systemu kontroli wersji. Kiedy przyrostek nie jest znany programowi make. musi zostać zdefiniowany poprzez użycie specjalnego polecenia .SUFFIXES (listing 6.1:2). W celu umożliwienia użytkownikom określania abstrakcyjnych nazw plików, w ramach reguł program make automatycznie przechowuje wiele zmiennych, modyfikując ich wartość w oparciu o obiekt docelowy oraz zależności przetwarzanej reguły. Najważniejsze zmien­ ne obsługiwane przez program make wymieniono w tabeli 6.3. Tabela 6.3. Zmienne obsługiwane przez program make Zmienna

Zawartość

t$

Znak S.

SI?

Nazwa tworzonego pliku. Nazwy plików młodszych od docelowego.

$? $> S
• < !- - pro p e rty values you must customize fo r successful b u ild in g !!! --> «property file -''b u 1 1 d .p ro p e rtie s "/» • -------------------------- Dziedziczenie właściwości «property f i l e - " . , /b u ild , p ro p e rtie s "/» na poziomie całego projektu «property «property «property «property

name-” bu1ld.com piler" v a lu e -” c la s s ic ” /> name-”webapps.build” v a lu e * " ../b u ild ” /» name-”webapps.dist" v a lu e - " . . / d is t " / » name-"webapp.name" value-'m anager"/»

• ---- Określenie właściwości projektu

« ta rg e t name*” b u ild -p re p a re "» • --------- Reguta «mkdir d i r - ” $(w ebapps.build}"/» • ------------- Polecenia • Polecenie sort -n -t . +1 -r | > head 1.30 sed/process.c 1.20 sed/complle.c 1.14 sed/sed.l 1.10 sed/main.c 1.8 sed/Makef1le 1.7 sed/defs.h 1.6 sed/misc.c 1.6 sed/extern.h 1.4 sed/P0$IX

Repozytorium systemu kontroli wersji można również użyć w celu zidentyfikowania sposobu implementacji określonych zmian. Weźmy pod uwagę poniższy wpis z reje­ stru, który występuje dla wersji 1.13 pliku cat .c. revisión 1.13 date: 1997/04/27 18:34:33: author: kleink: State: Exp. Unes: +8 -3 Indicate file handling failures by exit codes >0: fixes PR/3538 from David Eckhardt .

W celu dokładnego sprawdzenia, w jaki sposób są obecnie wskazywane błędy związane z obsługą plików, możemy wydać polecenie badające (kontekst — patrz podrozdział 10.4) różnice między wersjami 1.12 a 1.13. $ cvs dlff -c -rl.12 -rl.13 basesrc/bin/cat/cat.c Index: basesrc/bin/cat/cat.c RCS f11e : /cvsroot/basesrc/bin/cat/cat.c.v retrieving revisión 1.12 retrieving revisión 1.13 diff -c -rl.12 -rl.13

Ü ljjiL iU iU iU J U *** 136.141 *** — 136.142 — fp - stdin else if ((fp - fopen(*argv. "r")) -- NULL) { warnC'Xs". *argv): rval - 1; ++argv: continué;

) Jak pokazuje powyższy fragment, został dodany wiersz ustawiający znacznik rval (określający prawdopodobnie wartość zwracaną przez polecenie), kiedy polecenie otwarcia pliku zakończy się niepowodzeniem.

Rozdział 6. ♦ Metody analizy dużych projektów

201

Ćwiczenie 6.16. W jaki sposób śledzisz zmiany wersji plików w swoim środowisku? Jeżeli nie jest używany system kontroli wersji, warto rozważyć jego wdrożenie. Ćwiczenie 6.17. Wyjaśnij, w jaki sposób plik wykonywalny mógłby zawierać różne znaczniki identyfikacyjne pochodzące z pewnych plików źródłowych. W jaki sposób można by rozwiązać ten problem? Ćwiczenie 6.18. Wiele projektów open-source udostępnia ogółowi użytkowników w trybie tylko do odczytu swoje repozytoria CVS. Zlokalizuj taki projekt i utwórz jego lokalną kopię na swoim komputerze. Używając polecenia cvs log. zbadaj rejestr określonego pliku (najlepiej takiego, który posiada rozgałęzienia) i dopasuj znaczniki symboliczne do wersji pliku. Wyjaśnij, w jaki sposób zostały wykorzystane rozgałę­ zienia w celu obsługi kolejnych publikacji.

6.6. Narzędzia związane z projektem Duże projekty często charakteryzują się na tyle unikatowymi problemami i posiadaniem odpowiednich zasobów, aby było możliwe skonstruowanie specjalizowanych narzędzi jako elementu procesu implementacji. Niestandardowe narzędzia są używane w wielu różnych kontekstach procesu rozwoju aplikacji, w tym w konfiguracji, zarządzaniu procesem budowy, generowaniu kodu, testowaniu oraz tworzeniu dokumentacji. Poniż­ szej zostaną przedstawione pewne adekwatne przykłady narzędzi używanych do wyko­ nywania wspomnianych zadań. Konfiguracja jądra systemu operacyjnego jest szczególnie złożonym zadaniem, gdyż wiąże się z utworzeniem niestandardowego jądra spełniającego wymagania określonej konfiguracji sprzętowej poprzez dokonanie wyboru spośród setek sterowników urzą­ dzeń oraz opcji programowych, w wielu wypadkach niezależnie od siebie. Konfigura­ cję jądra systemu NetBSD obsługuje program config ' 4 — narzędzie, które odczytuje konfigurację opisującą opcje systemu i tworzy opis urządzeń wejścia-wyjścia, które mogą zostać dołączone do systemu, oraz plik makefile służący do budowy określonego jądra. Dane wejściowe programu config mają następującą formę25. # CPU options options CPU_SA110

# Support the SA110 core

# Architecture options options IOMD options RiSCPC

# We have an IOMD # We are a RiscPC

# File systems file-system FFS file-system MFS file-system NFS

# UFS # memory file system # Network file system

[• J # Open Firmware devices

24 netbsdsrc/usr.sbin/config. ~5nelhsdsrc/sys/arch/arm32/conf/GENERJC.

202

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source ofbus* ofbus* ofrtc*

at root at ofbus? at ofisa?

W oparciu o takie dane wejściowe program config generuje plik makefile, zawierający tylko te pliki, które mają zostać skompilowane dla celów danej konfiguracji oraz plik ioconf .c, który definiuje struktury systemu dla wybranych urządzeń, jak poniżej. /* file systems */ extern struct vfsops ffs_vfsops; extern struct vfsops mfs_vfsops: extern struct vfsops nfs_vfsops:

[...] struct vfsops *vfs_list_imt1al[] - ( &ffs_vfsops. &mfs_vfsops. infs vfsops.

[ .. .I NULL.

Specjalizowane narzędzia są również często wykorzystywane w celu obsługi procesu budowy, szczególnie w przypadku, gdy projekt jest przenoszony na wiele różnych plat­ form, co uniemożliwia użycie narzędzi związanych z konkretną platformą. Przykładowo, system X Window używa programu imake20 — niestandardowego procesora plików makefile — w celu zarządzania procesem budowy. Program imake jest używany w celu generowania plików makefile na podstawie szablonu, zestawu makrofunkcji preproce­ sora języka C oraz pliku wejściowego (po jednym na każdy katalog). Jest on wykorzy­ stywany do rozdzielenia elementów zależnych od maszyny (takich jak opcje kompilatora, zmienne nazwy poleceń oraz specjalne reguły budowy) i opisów różnych obiektów pod­ legających budowie. Stąd ogólnosystemowy plik konfiguracyjny jest używany do utwo­ rzenia ponad 500 odrębnych plików makefile. Ponadto architekci systemu X Window nie mogli polegać na istnieniu określonego generatora zależności dla plików nagłów­ kowych języka C. Dlatego też opracowali specyficzny dla projektu generator o nazwie makedepencf1. Bez wątpienia najczęściej spotykanym użyciem narzędzi specyficznych dla danego pro­ jektu jest generowanie specjalizowanego kodu. Takie narzędzie jest wykorzystywane do dynamicznego tworzenia kodu programu w ramach procesu budowy. W większości przypadków kod jest napisany w tym samym języku co reszta systemu (na przykład C) i rzadko bywa wyrafinowany, zwykle składając się z tabel przeglądowych lub prostych instrukcji switch. Narzędzia są używane do generowania kodu w czasie budowy w celu uniknięcia narzutu związanego z wykonywaniem analogicznych operacji w czasie uru­ chomienia. W ten sposób kod wynikowy jest często wydajniejszy i zajmuje mniej pamię­ ci. Dane wejściowe narzędzia generującego kod to inny plik reprezentujący w formie tekstowej specyfikacje generowanego kodu, prosty język dedykowany lub fragmenty kodu źródłowego systemu. Weźmy po uwagę przykład emulatora terminala IBM 3270. Składa się on z 77 plików, więc stanowi projekt średniego rozmiaru. Jednak wykorzy­ stuje on cztery różne narzędzia w celu utworzenia odpowiednich odwzorowań między ~hXFree86-3.3/xc/config/imake. XFree86-3.3/xc/config/makedepend.

Rozdział 6. ♦ Metody analizy dużych projektów

203

znakami i kodami klawiaturowymi IBM a ich odpowiednikami używanymi w przypad­ ku uniksowych stacji roboczych. Na rysunku 6.11 przedstawiono sposób użycia tych narzędzi w czasie procesu budowy. Dwa różne narzędzia. mkastods's oraz mkdstoas“9, tworzą odwzorowania między znakami ASCII a EBCDIC (specyficznymi dla produk­ tów firmy IBM) i odwrotnie. Pliki wyjściowe tworzone przez te narzędzia, asc disp.out oraz d isp a sc .o u t, są następnie umieszczane w pliku języka C o nazwie disp_asc.c30, który podlega kompilacji w ramach pliku wykonywalnego emulatora terminala tn3270. Podobnie, narzędzie mkastosc31 jest używane do tworzenia odwzorowania między kodami ASCII a kodami klawiszy IBM, zaś narzędzie mkhits32 służy do utworzenia odwzoro­ wania między kodami klawiszy z uwzględnieniem klawisza Shift a funkcjami i znakami terminala IBM 3270. Oba narzędzia generują kod poprzez wstępne przetworzenie uniksowego pliku definicji klawiatury unix.kbd33. pliku poleceń terminala IBM 3270 host » • c tlr .h 34 oraz plików definicji funkcji edycyjnych function.h35 oraz function.c36 (w przetworzonej wstępnie postaci jako TMPfunc.out). K.od generowany przez te na­ rzędzia (astosc.out oraz kbd.out) zostaje następnie umieszczony w plikach języka C (asto sc. c37 oraz 1nbound. c38) w postaci tablicy definicji struktur używanych jako tabele przeglądowe. Przykładowo, plik kbd.out będzie zawierał odwzorowania klawiszy na­ stępującego rodzaju: struct hlts h1ts[] - { ( 0. { (undefined). {undeflned}. (undeflned), (undefined)

) 11 70. ( /* 0x05 */ { FCN_ATTN ). { undeflned }. { FCNJUD. AID_TREQ ). { undefined },

1 1.

f 65. ( /* 0x06 */ { FCN_AID. AID_CIEAR ). { undefined }.

{ FCN_TEST ), { undefined ).

1 }• Narzędzia związane z projektem są również często wykorzystywane w celu obsługi testów regresyjnych produktu. Interesującym przykładem jest tu zestaw testowy użyty w przypadku implementacji języka Perl. Zestaw składa się z ponad 270 przypadków 28

29

netbsdsrc/usr. bin/tn3270/tools/mkastods/mkastods. c.

‘ netbsdsrc/usr. bin/tn3270/tools/mkdstoas/mkdstoas. c. 30netbsdsrc/usr. bin/tn3270/api/disp_asc.c. 31 netbsdsrc/usr. bin/tn32 70/tools/mkastosc/mkastosc. c. 32netbsdsrc/usr.bin/tn3270/tools/mkhits/mkhits.c. 33 netbsdsrc/usr.bin/tn3270/ctrl/unix.kbd. 34 netbsdsrc/usr.bin/tn3270/ctrl/hostctlr.h. 35netbsdsrc/usr. bin/tn3270/ctrl/function.h. 36netbsdsrc/usr. bin/tn3270/ctrl/function. c. 37netbsdsrc/usr.bin/tn3270/api/astosc.c. 38

netbsdsrc/usr. hin/tn3270/ctrl/inbound. c.

204

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 6.11.

Narzędzia używane podczas procesu budowy emulatora terminala IBM 3270

testowych. Program sterujący TEST39 jest używany w celu uruchomienia każdego testu i utworzenia raportu wyników. Co interesujące, również został napisany w języku Perl — ze szczególną ostrożnością, tak aby uniknąć użycia konstrukcji, które mogłyby po­ wodować błędy w czasie procesu testowania. Wreszcie, tego rodzaju narzędzia są również wykorzystywane w celu zautomatyzo­ wania generowania dokumentacji projektu. Program javadoc, obecnie rozprowadzany jako narzędzie ogólnego przeznaczenia w ramach pakietu Java SDK. najprawdopodob­ niej na początku stanowił narzędzie służące do tworzenia dokumentacji dla interfejsu Java API. Dokonuje on analizy składniowej deklaracji oraz komentarzy w zbiorze pli­ ków źródłowych języka Java i tworzy zbiór stron HTML opisujących klasy, klasy we­ wnętrzne, interfejsy, konstruktory, metody oraz pola. Adekwatny przykład kodu języka Java z komentarzami uwzględniającymi wymagania narzędzia javadoc przedstawiono na listingu 6.440. Listing 6.4. Komentarze specyficzne dla narzędzia javadoc zawarte w pliku języka Java____________ /** Retrieves the value of a JDBC CHAR. VARCHAR. * or tONGVARCHAR parameter as a String in * the Java programing language. *

- Opis metody

*

* * * * * * * *

For the flxed-length type JDBC CHAR, the String object returned has exactly the same value the JDBC CHAR value had 1n the database, including any padding added by the database. @param parameterlndex the first parameter 1s 1. the second 1s 2. • --and so on (»return the parameter value. If the value 1s SQL NULL, the result • — 1s null (»exception SQLException if a database access error occurs • ----------

- Opis parametru -Zwracana wartość -Zgłaszany wyjątek

*/ public String getStnngtmt parameterlndex) throws SQLException { • ------

39perl/t/TEST. 40 hsqldb/src/org/hsqldb/jdbcPreparedStatement.java: 837 851.

-Deklaracja

Rozdział 6. ♦ Metody analizy dużych projektów

205

Dystrybucja języka Perl. opracowana przez zespól entuzjastycznych twórców narzędzi, również zawiera zestaw narzędzi opartych na języku znaczników pod (plain old docu­ mentation — zwykła dokumentacja), które są używane do tworzenia dokumentacji w wielu różnych formatach. Narzędzia pod1' potrafią utworzyć dane wyjściowe w for­ macie LaTeX. uniksowych stron podręcznikowych, zwykłego tekstu oraz HTML. Podob­ nie jak narzędzie javadoc, również ich zakres stosowania rozszerzy! się w porównaniu z początkowymi zamierzeniami twórców i obecnie stanowią narzędzia ogólnego prze­ znaczania służące do tworzenia dokumentacji. Kolejny aspekt dokumentacji często obsługiwany przez niestandardowe narzędzia to komunikaty o błędach. Zwykle są one rozrzucone po dużych obszarach kodu programu. W przypadku niektórych projektów opracowano narzędzie służące do lokalizowania komunikatów o błędach w kodzie (przez wyszukiwanie, na przykład, wywołań funkcji, która je tworzy) oraz generowania odpo­ wiedniej dokumentacji użytkowej — często poprzez wykorzystanie specjalnie sforma­ towanych komentarzy, które występują obok komunikatu. Ćwiczenie 6.19. Zbadaj program konfiguracyjny jądra systemu config42 na płycie dołą­ czonej do książki i określ elementy systemu, które może on konfigurować. Ćwiczenie 6.20. W jaki sposób jest określany proces instalacji w twoim ulubionym zin­ tegrowanym środowisku programowania? Omów zalety i wady stosowanego podejścia. Ćwiczenie 6.21. Zaproponuj sposób lokalizowania narzędzi generujących kod na płycie dołączonej do książki. Zlokalizuj pięć takich narzędzi i wyjaśnij cel ich użycia. Ćwiczenie 6.22. Zbadaj zautomatyzowany procesor testowy TEST4' języka Perl oraz któryś przypadek testowy, na przykład a rra y .t44. Wyjaśnij, w jaki sposób należy pisać przypadki testowe oraz w jaki sposób je przetwarzać podczas testowania regresyjnego. Ćwiczenie 6.23. Zamień swój życiorys na format pod Perlą. Używając narzędzi pod, przekonwertuj go do formatu HTML oraz zwykłego tekstu. Oceń użyteczność takiego podejścia.

6.7. Testowanie Dobrze zaprojektowane projekty uwzględniają testowanie wszystkich lub części ich elementów. Takie działania mogą stanowić część planu weryfikacji poprawności dzia­ łania systemu lub pozostałość mniej formalnych działań testowych prowadzonych przez twórców systemu w czasie jego implementowania. W ramach zwiększania swoich umie­ jętności czytania kodu, warto nauczyć się rozpoznawać i analizować kod testujący oraz przypadki testowe, a następnie używać konstrukcji testujących jako pomocy w zrozu­ mieniu reszty kodu. W poniższych paragrafach zostanie omówionych wiele różnych typów kodu testującego, z jakim można się spotkać. 41perl/pod. 42netbsdsrc/usr.sbin/config. 43perl/t/TEST. 44perl/t/op/array.i.

206

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Najprostszy rodzaj kodu używanego w celach testowania to instrukcja generująca wyj­ ściowe dane rejestrujące lub diagnostyczne. Takie instrukcje są zwykle używane przez programistów w celu sprawdzenia, czy działanie programu jest zgodne z oczekiwania­ mi. Diagnostyczne dane wyjściowe programu mogą pomóc w zrozumieniu najważniej­ szych części przebiegu sterowania programu oraz elementów danych. Ponadto miejsca, w których znajduje się instrukcje śledzące, zazwyczaj określają istotne fragmenty funk­ cjonowania algorytmu. W przypadku większości programów informacje diagnostyczne są wysyłane na standardowe wyjście programu lub do pliku45. #i fdef DEBUG 1f (trace “ NULL) trace - fopen("bgtrace". "w"): fprintft trace. "\nRoll: ?d td*s\n". DO. Dl. race? " (race)" : ""}; fflush(trace): lendif

F I Warto w powyższym przykładzie zauważyć, w jaki sposób makro DEBUGjest używane do zachowania kontroli nad tym, czy określone fragmenty kodu będą kompilowane w ramach końcowego pliku wykonywalnego. Wersje produkcyjne systemów są zwykle kompilowane bez definiowania makra DEBUG, tak więc kod śledzący oraz generowane przez niego dane wyjściowe nie występują. Programy działające w tle (na przykład demony systemu Unix lub usługi systemu Win­ dows) wymagają bardziej wyrafinowanego sposobu generowania informacji śledzących, szczególnie jeśli jednocześnie działa wiele realizacji lub wątków tego samego programu. W przypadku systemów uniksowych funkcja biblioteczna syslog służy do dołączania informacji do odpowiedniego dziennika systemowego44. syslog(L0G_DE8UG. "Successful lookup: % d . %0 lport. fport. pwp->pw_name):

Ss\n".

W przypadku systemu Microsoft Windows funkcja używana do generowania adekwat­ nego rodzaju danych nosi nazwę ReportEvent, zaś programy Java często wykorzystują darmową bibliotekę log4j w celu zapewnienia organizacji i zarządzania wydajnym generowaniem komunikatów rejestrujących. Zbyt duża ilość informacji śledzących może zaciemnić obraz kluczowych komunikatów lub spowolnić działanie programu. Z tego względu w programach często definiuje się wartość całkowitą noszącą nazwę poziomu diagnostyki (ang. debug level), wykorzy­ stywaną do filtrowania generowanych komunikatów 1. if (debug > 4) pr1ntf("syst1ine: offset *s\n". lfptoatnow. 6)):

W powyższym przykładzie komunikat pojawi się tylko wówczas, gdy poziom diagno­ styki jest większy od 4. Wyższy poziom oznacza, że program generuje więcej komuni­ katów. W celu zmiany tego poziomu używana jest opcja programu lub znacznik kon­ figuracyjny. W przypadku języków obsługujących wyjątki, takich jak Ada, C++, C# i Java, komunikaty diagnostyczne często stanowią część kodu obsługi wyjątku'15. 45 netbsdsrc/games/haekgammon/backgammon/move.c: 380 - 385. 44 netbsdsrc/libexec/identd/parse.c: 372 - 373. 47 netbsdsrc/lib/libntp/systime.c: 216-217. 4*jl4/catalina/src/share/org/apache/catalina/core/Sta>idard(Vrapper ValveJava: 295 -301.

Rozdział 6. ♦ Metody analizy dużych projektów

207

try { 1f (servlet !- null) { wrapper.deal 1ocate(serv1et),

) } catch (Throwable e) { log(sm.getStr1ng("standardWrapper.deallocateException". wrapper. getNameO). e);

Instrukcje śledzące, które omówiliśmy, mogą generować duże ilości danych wyjścio­ wych, jednak w większości przypadków nie pozwalają nam stwierdzić, czy dane te są poprawne. Tylko w nielicznych przypadkach instrukcja diagnostyczna wyświetla popraw­ ną, oczekiwaną wartość49. printfCfree tu bytes @tlx. should be : assert(FLT_ROUNOS — 1): ex1t(0):

) Asercji, które testują całe funkcje, można używać jako instrukcji specyfikacji dla każdej danej funkcji.

A Badając kod, należy pamiętać, że wyrażenie zawarte w makrodefinicji assert zwykle nie jest kompilowane w produkcyjnej wersji programu, to znaczy makro NODEBUGjest wówczas ustawiane na wartość 1. W takim przypadku asercje powodujące pewne skut­ ki uboczne (takie jak wywołanie funkcji fpsetround w przedstawionym powyżej przy­ kładzie) nie będą zachowywać się zgodnie z oczekiwaniami. Programiści języka Java często organizują asercje w ramach pełnych przypadków te­ stowych używając modelu JUnit autorstwa Kenta Becka i Ericha Gamma’ego. J 11nit obsługuje inicjalizację danych testowych, definiowanie przypadków testowych, ich organizację w ramach zestawu testowego oraz zbieranie wyników testów. Oferuje rów­ nież narzędzie graficzne TestRunner służące do wykonywania przypadków testowych. Na listingu 6.5 przedstawiono najważniejsze elementy procesu konfiguracji testu przy użyciu JUnit56 7. Przypadki testowe są zorganizowane w klasę dziedziczącą po klasie TestCase. Pola klasy są używane do przechowywania danych potrzebnych między wyko­ naniami różnych przypadków testowych (listing 6.5:1). Metoda o nazwie setllp jest odpowiedzialna za inicjalizację wartości pól, natomiast metoda tearDown za zniszczenie f il elementów przydzielonych do określonego przypadku testowego. Przypadki testowe są pisane w formie odrębnych metod, których nazwy rozpoczynają się od słowa test. W każdym przypadku testowym jest zawartych wiele różnych typów metod assert używanych do weryfikacji otrzymanych wyników. Wiele przypadków testowych zaim­ plementowanych w klasie może zostać zebrane razem w ramach metody zestawu testo­ wego (ang. test suite) o nazwie suite. W opisywanym przypadku (listing 6.5:2) kon­ struktor zestawu zbiera z klasy wszystkie metody publiczne o nazwie rozpoczynającej się od te s t. Listing 6 .5 . Użycie modelu testującego JUnit public abstract class BaseDirContextTestCase extends TestCase { •

Kla\apoditawtmvMril

protected DlrContext context * null: • ------------------------ [l] Stan testu

[...] 1 public class WARDirContextTestCase extends BaseDirContextTestCase (

if'jt4/catalina/src/test/org/apache/naming/resources/BaseDirContextTestCase.java. '' jt4/catalina/src/test/org/apache/naming/resources/WARDirContextTestCase Java.

210

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source public void setUpO { • ---context - new WARDirContextO: 1 && argv[l][0] — switch(argv[l][l]) (

(

[...] case 's': sflag - 1: break; default: usaget):

I

argc--: argv-H-:

1 nflles - argc - 1:

Porównajmy powyższy kod z alternatywnym rozwiązaniem standardowym wykorzy­ stującym funkcję biblioteczną getopt22. whlle ((ch - getopt(argc. argv. "mo:ps:tx")) !- -1) switch(ch) {

[. .] case 'x': 1f (!domd5)

requiremd5C-x"): MDTestSuiteO: norod5stdin - 1. break; case default: usaget):

)

argc -- optind: argv += optind;

Strona podręcznikowa pomocy dla funkcji getopt została przedstawiona na rysunku 2.1 na stronie 43. 21netbsdsrc/usr.bin/checknr/checknr.c: 209 -259. •>2 netbsdsrc/usr.bMcksum/cksum.c: 102- 155.

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

226

Druga wersja oprócz tego. że jest łatwiejsza do zrozumienia, występuje w dokładnie tej samej postaci we wszystkich programach języka C, które wykorzystują funkcję getopt, co pozwala czytelnikowi kodu utworzyć w umyśle wiemy wzorzec użycia i natych­ miast dostrzegać wszelkie odstępstwa od niego. Opracowano dziesiątki różnych dosko­ nałych sekwencji kodu służących do analizy przekazywanych parametrów — jednak każda z nich wymagała włożenia sporego wysiłku w jej opracowanie i sprawdzenie. Korzyści wynikające z użycia standardowych konstrukcji związanych z interfejsem użytkownika objawiają się w jeszcze większym stopniu w aplikacjach z interfejsem GUI. Różne wskazówki zalecane przez firmę Microsoft określają, że należy używać gotowych formantów interfejsu, takich jak okna dialogowe wyboru pliku, koloru lub czcionki, zamiast opracowywać własne rozwiązania. Zawsze kiedy funkcjonalność gra­ ficznego interfejsu użytkownika zostanie zaimplementowana przy użyciu odpowiednich I konstrukcji programistycznych, poprawność przyjęcia specyfikacji danego interfejsu 1 może zostać w banalny sposób zweryfikowana przez zwykłe przejrzenie kodu. Ćwiczenie 7.6. Utwórz listę praktyk programistycznych, które mogą zw iększyć czytel­ ność kodu. Tam, gdzie to możliwe, spróbuj odwołać się do istniejących przykładów.

7.6. Standardy związane z procesem rozwojowym System programistyczny to nie tylko zbiór elementów kodu. Wiele standardów zapisu kodu dotyczy również innych dziedzin procesu rozwojowego, w tym tworzenia doku­ mentacji oraz organizacji procesu konsolidacji i publikacji projektu. Każdy system wskazówek określa przynajmniej standardowe dokumenty i formaty ich zapisu. Dokumentacja użytkowa jest zwykle najlepiej zorganizowana ze względu na jej bliski związek z aplikacją lub procesem publikacji. Aplikacje systemu Microsoft Win­ dows zwykle zawierają plik pomocy, projekty GNU udostępniają podręcznik w forma­ cie Texinfo, zaś typowe aplikacje uniksowe zawierają standardowe strony podręcznika I pomocy. Omówienie dokumentacji i jej wpływu na łatwość czytania kodu Czytelnik | znajdzie w podrozdziale 8.2.

m

23

Jak stwierdzono w podrozdziale 6.3, nie istnieje jeden uniwersalny, standardowy sposób j określenia i przeprowadzenia procesu konsolidacji, co utrudnia zrozumienie szczegółów I dotyczących różnych mechanizmów konsolidacji. Wiele wskazówek może być tutaj pomocne przez określenie ścisłych reguł sposobu organizacji tego procesu. Reguły te często są oparte na określonych narzędziach lub standardowych makrodefmicjach. Kie- j dy już się je pozna, można szybko zrozumieć proces budowania projektu, który prze- [ biega zgodnie z nimi. Aby docenić różnice pod względem złożoności odpowiednicl opisów, jakie może ze sobą nieść standardowy proces zbudowania projektu, wystar­ czy porównać 18-wierszowy plik makefile1' użyty do kompilacji 64 plików bibliotek

netbsdsrc/lib/libntp/Makefile.

Rozdział 7. ♦ Standardy i konwencje pisania kodu

227

protokołu NTP w systemie NetBSD z szablonem pliku makefileZA składającym się z 40 ręcznie oraz 37 automatycznie wygenerowanych wierszy, używanym do kompilacji 12 plików biblioteki obsługi serwera apache. Proces publikacji systemu również jest często szczegółowo opisany, zwykle w celu zapewnienia dostosowania do wymagań procesu instalacji aplikacji. Standardowe for­ maty dystrybucji systemów, takie jak instalator systemu Windows lub format RPM systemu Red Hat Linux, narzucają ścisłe reguły na typy plików, które składają się na dystrybucję, kontrolę wersji, katalogi instalacyjne oraz opcje kontrolowane przez użyt­ kownika. Badając proces publikacji systemu, jako podstawę często można wykorzystać wymagania odpowiedniego formatu dystrybucyjnego. Ćwiczenie 7.7. Zidentyfikuj standardy związane z procesem rozwojowym stosowane w Twojej organizacji. Wyjaśnij, jakie elementy programistyczne (pliki, katalogi) muszą zostać zbadane w celu zweryfikowania dostosowania danego systemu do tych stan­ dardów.

Dalsza lektura Do ważnych konwencji zapisu kodu należą style: Indian Hill [CEK']. GNU Coding Stan­ dards [S+01], styl plików źródłowych jądra systemu FreeBSD [Fre95] oraz Java Code Conventions [Sun99a]. Wskazówki co do zapisu kodu można również znaleźć w kla­ sycznej pozycji Kemighana i Plaugera [KP78], nowszej książce Kemighana i Pike’a [KP99] oraz w serii artykułów autorstwa Spencera [Spe88, SC92. Spe93], Notacja węgierska kodowania zmiennych po raz pierwszy została opisana w pracy doktorskiej Simonyi’ego [Sim76]. Bardziej dostępną i czytelniejszą wersję stanow i jego później­ sza praca [Sim99].

"J

apache/src/ap/Makefile, tmpl.

Rozdział 8.

Dokumentacja Każdemu niebanalnemu projektowi programistycznemu zwykle towarzyszą różne for­ malne i nieformalne elementy dokumentacji W niniejszym rozdziale zostaną przed­ stawione typowe rodzaje dokumentacji, jakie można spotkać w czasie badania projek­ tów i zaprezentowane przykłady tego, w jaki sposób mogą one pomóc w zrozumieniu kodu. Ponadto zostaną bliżej zbadane pewne klasy błędów dotyczących dokumentacji oraz ogólnie opisane niektóre formaty dokumentacji często używane w przypadku opro­ gramowania open-source. Wysiłki związane z czytaniem kodu należy wspierać wszel­ kiego rodzaju dostępną dokumentacją. Parafrazując powiedzenie zalecające użycie bi­ bliotek: minuta lektury dokumentacji może zaoszczędzić godzinę czytania kodu.

8.1. Rodząje dokumentacji Tradycyjnie przygotowywany projekt generuje w okresie swojego rozwoju w iele róż­ nych dokumentów. Kiedy są one odpowiednio zarządzane, mogą w ogromnym stopniu pomóc w zrozumieniu założeń, projektu i implementacji systemu. Chociaż istnieje bar­ dzo wiele odmian typów dokumentacji tworzonej w ramach różnych projektów, poniżej zostaną opisane pewne reprezentatywne dokumenty, z jakimi można się zetknąć. Dokument specyfikacji systemu (ang. system specification document) opisuje szczegóły dotyczące celów tworzenia systemu, jego wymagań funkcjonalnych, ograniczeń o cha­ rakterze zarządczym i technicznym oraz parametrów kosztowych i czasowych. Doku­ mentu tego należy używać w celu zrozumienia cech środowiska, w którym będzie funk­ cjonował czytany kod. Ten sam kod służący do kreślenia wykresów będzie się czytać inaczej w przypadku wygaszacza ekranu niż w przypadku modułu systemu sterowania reaktorem nuklearnym. Specyfikacja wymagań programowych (ang. software requirements specification) ofe­ ruje wysokopoziomowy opis wymagań użytkowników oraz ogólną architekturę systemu, prezentując szczegółowo wymagania funkcjonalne i niefunkcjonalne, takie jak przetwa­ rzanie, interfejsy zewnętrzne, bazodanowe schematy logiczne oraz ograniczenia projek­ towe. Ten sam dokument może również zawierać opis przewidywanych charakterystyk

230

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

rozwoju systemu wynikających ze zmian w środowisku programowym i sprzętowym oraz potrzeb użytkowników. Takiej specyfikacji należy używać jako punktu odniesie­ nia dla lektury i oceny kodu. Specyfikacja projektowa (ang. design specification) zawiera opis architektury syste­ mu, struktur danych i kodu. jak również interfejsów łączących różne moduły. Projekt zorientowany obiektowo prezentuje klasy podstawowe i metody publiczne systemu. Szczegółowe specyfikacje projektowe zwykle zawierają również określone informacje na temat każdego modułu (lub klasy), takie jak wykonywane przez niego działania, opis oferowanego interfejsu oraz jego powiązania z innymi modułami lub klasami. Ponadto można tu znaleźć opisy używanych struktur danych oraz stosowanych schematów ba­ zodanowych. Specyfikację taką należy wykorzystywać jako przewodnik po strukturze kodu oraz jego poszczególnych elementach. Specyfikacja testowa (ang. test specification) systemu stanowi opis planu przeprowa­ dzania testów, określonych procedur testowych oraz faktycznych wyników wykona­ nych testów. Każda procedura testowa opisuje moduły, które bada (co daje wyobrażenie o tym, jakie moduły służą do przetwarzania określonych danych wejściowych) oraz dane dla przypadków testowych. Dokument ten zapewnia dostęp do danych, których można użyć w celu sprawdzenia „na sucho” kodu, który się czyta. Wreszcie, to, co ogólnie określa się mianem dokumentacji użytkownika (ang. user docu­ mentation) składa się z wielu różnych dokumentów, w tym opisów funkcjonalnych, instrukcji instalacyjnych, opisów wprowadzających, podręcznika użytkownika oraz podręcznika administratora. Dużą korzyścią wynikającą z posiadania dokumentacji użytkowej jest to, że często jest to jedyny rodzaj dokumentacji, do jakiej ma się dostęp. W przypadku nieznanego systemu opis funkcjonalny oraz podręcznik użytkownika mogą zapewnić dostęp do istotnych informacji, pomagających w lepszym zrozumieniu kontekstu kodu, który się czyta. Podręcznika użytkownika można użyć w celu szybkie­ go znalezienia dodatkowych informacji odnośnie do komponentów kodu związanych z logiką warstwy prezentacji i aplikacji, zaś podręcznik administratora pozwala znaleźć szczegóły dotyczące interfejsów, formatów plików oraz komunikatów o błędach, jakie można spotkać w kodzie. Ćwiczenie 8.1. Wybierz trzy duże projekty z płyty dołączonej do książki i określ doku­ mentację, jaką oferują. Ćwiczenie 8.2. Omów zakres zastosowania omówionych typów dokumentacji w przy­ padku projektów open-source.

8.2. Czytanie dokumentacji Jeżeli Czytelnik wątpi w istnienie prawdziwie przydatnej dokumentacji, poniżej znaj­ dzie opis konkretnych przykładów wykorzystania dokumentacji dla własnych korzyści. Dokumentacja stanowi szybki sposób poznania ogólnego obrazu systemu i zrozumienia kodu, który jest związany z określoną funkcjonalnością. Weźmy pod uwagę implemen­ tację szybkiego systemu plików (ang. fast file system) w systemie Berkeley Unix. Opis

Rozdział 8. ♦ Dokumentacja

231

systemu plików w podręczniku systemowym zajmuje 14 stron, na których zawarto objaśnienie organizacji systemu, kwestie parametryzacji, strategie rozkładu, kwestie wydajnościowe oraz rozszerzenia funkcjonalne. Lektura tego tekstu1 2 wymaga o wiele mniejszego nakładu sił niż zbadanie 4586 wierszy kodu źródłowego, które składają się na system3. Nawet badając niewielkie fragmenty kodu można łatwiej zrozumieć ich funkcjonowanie, kiedy zrozumie się stawiane im cele dzięki lekturze dokumentacji. Weźmy pod uwagę poniższy fragment4. line - gobble » 0; for (prev - '\n': (eh - getc(fp)) !• EOF: prev - ch) { if (prev — '\n') { if (ch - '\n') ( 1f (sflag) { if ((gobble && putchar(ch) — EOF) break: gobble - 1; contlnue:

) [...] 1 1

gobble - 0:

[...] ) Spróbujmy sobie wyobrazić próbę jego zrozumienia przed i po lekturze dokumentacji dotyczącej opcji -s polecenia cat, ustawiającej identyfikator sflag5' 6. -s Scala wiele sąsiadujących pustych wierszy, co sprawia, że na wyjście zostaje przekazana pojedyncza spacja. Dokumentacja zapewnia dostęp do specyfikacji, względem których należy badać kod. Jako punktu wyjścia można użyć specyfikacji funkcjonalnej. W wielu przypadkach kod obsługuje określony produkt lub standardowy interfejs, tak więc nawet jeśli nie można znaleźć specyfikacji funkcjonalnej dla danego systemu, można wykorzystać odpowied­ ni standard jako wskazówkę. Weźmy pod uwagę zadanie zbadania zgodności serwera WWW apache ze standardem protokołu HTTP. W kodzie źródłowym serwera apache można znaleźć następujący fragment7. swltch (*method) ( case 'H': if (strempimethod. "HEAD") — 0) return M_GET. /* see header_only 1n request_rec */ break. case 'G': tf (strempimethod. "GET”) — 0) return M_GET. break:

1 netbsdsrc/share/doc/smm/Q5.fastfs. doc/ffs.pdf. netbsdsrc/sys/ufs/ffs. 4 netbsdsrc/bin/eat/cat.c: 159-207. 5 netbsdsrc/bin/cat/cal.l. 6 doc/cat.pdf. apache/src/main/httpjprotocol.c: 749 - 764.

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

232

case 'P*: if (strcmptmethod. “POST") — 0) return H_P0ST; if (strcmptmethod. "PUT") — 0) return M_PUT; if (strcmptmethod. "PATCH") “ 0) return M_PATCH;

Można z łatwością zweryfikować kompletność zaimplementowanych poleceń, jak rów­ nież istnienie rozszerzeń, poprzez porównanie ich z dokumentem specyfikacji protokołu HTTP — RFC-20688. The Method token indicates the method to be performed on the resource identified by the Request-URI. The method is case-sensitive. Method

* | | | | | | |

"OPTIONS" "GET" "HEAD" "POST" "PUT" "DELETE" "TRACE" extension-method

: Section 9.2 : Section 9.3 : Section 9.4 : Section 9.5 ; Section 9.6 : Section 9.7 ; Section 9 8

Dokumentacja często odzwierciedla, a przez to ujawnia, strukturę systemu. Adekwatny przykład stanowi tu podręcznik administratora serwera poczty sendm aif Spójrzmy na pliki źródłowe systemu11. arpadate.c. dock.c. collect.c. conf.c. convtime.c. daemon.c. deliver.c. domain.c. envelope.c. err.c. headers.c. macro.c. maln.c. map.c. mci.c. mime.c. parseaddr.c. queue c. readcf.c. recipient.c. safefile.c. savemail.c. srvsmtp.c. stab.c. stats.c. sysexits.c. trace.c. udb.c. usersmtp.c. util.c. version.c

W tabeli 8.1 pokazano, że wiele z nich odpowiada określonym nagłówkom dokumentacji. Tabela 8.1. Pliki źródłowe odpowiadające nagłówkom dokumentacji serwera sendmail Nagłówek dokumentacji

Plik(i) źródłowy(e)

2.5

Configuration file

rea dcf.c

3.3.1

Aliasing

a lia s . c

3.4

Message collection

c o lle c t .c

3.5

Message delivery

d e liv e r .c

3.6 3.7

Queued messages Configuration

queue.c

3.7.1

Macros

macro.c

3.7.2 3.7.4

Header declarations

headers.c, envelope.c

Address rewriting rules

parseaddr.c

co n f.c

8 doc/rfc2068.txr. 1913-1923. 9 netbsdsrc/usr.sbin/sendmail/doc/intro. 10doc/sendmail.pdf. 11 netbsdsrc/usr.sbin/sendmaił/src.

233

Rozdział 8. ♦ Dokumentacja

Dokumentacja pomaga w zrozumieniu skomplikowanych algorytmów i struktur danych. Przebieg wyrafinowanych algorytmów zwykle trudno jest śledzić i zrozumieć. Kiedy zostaną one przepisane do postaci wydajnego kodu, mogą się stać całkowicie nieczy­ telne. Niekiedy dokumentacja kodu oferuje szczegóły dotyczące użytego algorytmu lub (częściej) komentarz zawarty w kodzie kieruje czytelnika do odpowiedniego źródła1'. * This algorithm is from Knuth vol. 2 (2nd ed). section 4.3.3. p 278.

Tekstowy opis algorytmu może sprawić, że zupełnie nieczytelny fragment kodu stanie się możliwy do zrozumienia. Weźmy po uwagę zadanie zrozumienia poniższego frag­ mentu13. for (arcp - memp->parents : arcp ; arcp - arcp->arc_parentl1st) {

[...] 1f ( headp -> npropcall ) { headp -> propfraction +- parentp -»propfraction * ( ( (double) arcp -> arc_count ) / ( (double) headp -> npropcall ) ):

) ) Teraz spróbujmy odwzorować kod na następujący opis algorytmu1'1 ' \ Niech Ce będzie liczbą wywołań pewnego podprogramu e, zaś C ' — liczbą wywołań podprogramu wywoływanego e przez program wywołujący r. Ponieważ zakładamy, że każde wywołanie podprogramu trwa czas średni w stosunku do wszystkich wywołań danego podprogramu, podprogram wywołujący jest związany z C ' /C,. czasu zajmowanego przez podprogram wywoływany. Niech Se będzie czasem własnym podprogramu e. Czas własny podprogramu można określić na podstawie informacji czasowych zebranych w czasie profilowanego wykonania programu. Wówczas całkowity czas Tn który chcemy przypisać podprogramowi r, określa poniższe równanie rekurencyjne: T ,= S ,+

£

7> ^-

r w yw otijt t

gdzie r wywołuje e jest relacją przedstawiającą wszystkie podprogramy e wywoływane przez podprogram r. Dokumentacja często pozwala odkryć znaczenie identyfikatorów występujących w ko­ dzie źródłowym. Weźmy pod uwagę poniższą makrodefinicję i związany z nią zwięzły komentarz16. #define TCPSJSTABUSHED 4 /* established */

12netbsdsrc/łib/libc/ąuad/muldii.c. 86. 13nelbsdsrc/usr.bin/gprof/arcs.c: 930 - 950. 14netbsdsrc/usr.bin/gprof/PSD.doc/postp.me: 71 -90. 15doc/gprof.pdf: s. 4. 16netbsdirc/sys/netinet/tcpJsm.h: 50.

234

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Makrodefinicja stanowi część kodu obsługi protokołu TCP/łP. Wyszukanie wyrazu ESTABLISHED w odpowiedniej dokumentacji (dokument RFC-793) pozwala odkryć na­

stępujący opis17: ESTABLISHED — reprezentuje otwarte połączenie, gdzie otrzymane dane mogą zostać dostarczone do użytkownika. Jest to normalny stan dla fazy przesyłania danych połączenia. Co jeszcze ważniejsze, ten sam dokument RFC zawiera rozbudowany rysunek ASCII prezentujący szczegóły modelu protokołu TCP jako maszyny stanów oraz dokładne przejścia między różnymi stanami (rysunek 8.1.). Badanie kodu obsługi protokołu TCP bez odpowiedniego diagramu stanów byłoby co najmniej nierozsądne. Rysunek 8.1.

........... \ CLOSED

Diagram stanów połączenia TCP

active OPEN

\

! ........... \

...................... \

\

à passive OPEN

CLOSE

create TC8

delete

c r e a t e TCB snd SYN \ \ \ V \

\ \

TCB

\

\

CLOSE LISTEN

delete rev SYN

I

snd SY N. AC K

/

TCB

SEND \

rev

SYK RCVD

\

|

snd SYN SYN SENT

SYN

snd ACK ♦

rev AC K

of SY N

\

x

j J

/

rev SYN.ACK snd ACK

CLOSE I



s n d FI N

ESTAB

CLOSE

I

♦ rev

I

snd FIN

/

rev FIN

\

\

FIN

snd ACK •*|

FIK WAIT-1 rev ACK of

CLOSE WAIT CLOSE

FI N

snd FIN V IF I N W A I T - 2 1



CLOSING



|

j

I LAST-ACK ♦ .....................

rev ACK of

re v A C K of FIK

rev FIN

Tlmeout-2NSL

\ snd ACK TIHE

♦ d el et e TCB W A I T ; .............

« »I

V CLOSED

Dokumentacja może nieść ze sobą uzasadnienie wymagań niefunkcjonalnych. Poniż­ szy fragment kodu pochodzi z kodu źródłowego serwera systemu nazw domenowych (D N S r. 1f (newdp->d_cred > dp->d_cred) ( /* better credibility. * remove the old datum.

*/ goto delete;

} Podobne decyzje dotyczące „credibility” (wiarygodności) danych występują w całym kodzie pliku źródłowego. Specyfikacja funkcjonalna DNS nie definiuje pojęcia wia­ rygodności danych ani nie dyktuje określonego zachowania w odniesieniu do danych 17 docZrfc793.txt: s. 21.

18

netbsdsrc/usr.sbinZnamedZnainedZdbnpdate.c: 447 - 452.

Rozdział 8. ♦ Dokumentacja

235

pochodzących z różnych źródeł. Jednakże zbiór dokumentów znajdujących się w kata­ logu doc aplikacji1J wymienia wiele problemów dotyczących bezpieczeństwa, związa­ nych z systemem DNS, zaś jeden z nich jest poświęcony zagadnieniu znacznikowania wpisów w pamięci podręcznej poziomami „wiarygodności” w celu aktualizowania danych w oparciu o ich jakość-0" 1. 5.1. Znacznikowanie buforowanych danych Obecnie BIND przechowuje dla każdego RR umieszczonego w pamięci podręcznej poziom „wiarygodności”, określający czy dane pochodzą ze strefy, z autorytatywnej odpowiedzi, sekcji uprawnień lub dodatkowej sekcji danych. Kiedy pojawi się bardziej wiarygodny zestaw' RR, starszy zestaw jest usuwany. Starsze wersje BIND kumulowały po prostu dane pochodzące ze wszystkich źródeł, nie przywiązując znaczenia do faktu, że pewne źródła są lepsze od innych. Zatem uzasadnienie wymagań niefunkcjonalnych — bezpieczeństwa — zostało opisane w dokumentacji, co pozwala czytelnikowi sprawdzić ich implementację w kodzie źró­ dłowym. Poza tym. dokumentacja systemowa często stanowi odzwierciedlenie toku myślenia projektanta co do celów i intencji definiowania wymagań systemowych, architektury oraz implementacji. Można również poznać odrzucone rozwiązania alternatywne oraz opis powodów ich odrzucenia. Przykładowa, weźmy pod uwagę uzasadnienie Pike’a i Thompsona przyjęcia w systemie operacyjnym Plan 9 schematu kodowania UTF Uni­ code zamiast reprezentacji 16-bitowej [PT93]. Standard Unicode definiuje odpowiedni zestaw znaków', ale nieodpowiednią reprezentację. Standard określa, że wszystkie znaki mają długość 16 bitów i są przesyłane w 16-bitowych jednostkach.... Aby przyjąć Unicode, musielibyśmy konwertować każdy tekst wchodzący i wychodzący z systemu Plan 9 między standardami ASCII a Unicode, co jest niemożliwe. W ramach pojedynczego programu, mającego kontrolę nad całością swoich danych wejściowych i wyjściowych, istnieje możliwość zdefiniow ania znaków jako wielkości 16-bitowych. W kontekście systemu sieciowego o setkach aplikacji działających na różnych maszynach pochodzących od różnych producentów jest to niemożliwe. Kodowanie UTF charakteryzuje kilka dobrych cech. Najważniejszą z nich jest ta, że bajt z zakresu standardu ASCII od 0 do 127 w formacie UTF ma taką samą postać. Zatem standard UTF jest zgodny wstecz ze standardem ASCII. Dokumentacja wyjaśnia wewnętrzne interfejsy programistyczne. Duże systemy zwykle dzielą się na mniejsze podsystemy współpracujące ze sobą poprzez wykorzystanie ści­ śle zdefiniowanych interfejsów. Ponadto niebanalne kolekcje danych są często organi­ zowane w postaci abstrakcyjnych typów danych lub klas o podobnie dobrze zdefiniowa­ nych interfejsach. Poniższe przykłady stanowią pewne typowe rodzaje dokumentacji API, z jakimi można się zetknąć. 19netbsdsrc/usr.shin/named/doc. netbsdsrc/usr.sbin/named/doc/misc/vixie-secuńty.ps. 21docMxie-security.pdf. s. 5.

236

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

♦ Dokumentacja programisty silnika bazy danych Hypersonic SQL hsąldb22. Dokumentacja, wygenerowana automatycznie na podstawie opatrzonego komentarzami kodu źródłowego Java przy użyciu systemu Together ControlCenter, może być czytana za pomocą przeglądarki internetowej. Oferuje w odrębnej ramce podgląd wszystkich klas w postaci diagramu UML, strukturę drzewiastą hierarchii pakietów oraz ogólny i szczegółowy widok pól, konstruktorów i metod każdej klasy. ♦ Opis wewnętrznych funkcji języka Perl oraz formatów reprezentacji danych na stronie podręcznikowej perlguts 2J' 24. Pierwszą stronę dokumentacji przedstawiono na rysunku 8.2. Warto zwrócić uwagę na zastrzeżenie umieszczone w sekcji Description (Opis). Ze względu na fakt, że dokumentacja bywa rzadko testowana i ogólnie jest pod tym względem traktowana z mniejszą uwagą niż faktyczny kod programu, często bywa błędna, niepełna lub nieaktualna. ♦ Dokumentacja funkcji i makrodefinicji systemu FreeBSD używanych w celu zarządzania pamięcią związaną z kodem obsługującym połączenia sieciowe25, 26. W rzeczywistości cała dziewiąta sekcja podręcznika systemów FreeBSD oraz NetBSD zawiera ponad 100 pozycji poświęconych wewnętrznemu funkcjonowaniu jądra systemu, gdzie opisano interfejsy funkcji oraz zmienne używane przez system i programistów piszących sterowniki urządzeń. Dokumentacja oferuje przypadki testowe i przykłady faktycznego użycia aplikacji. Kod źródłowy lub dokumentacja funkcjonalna systemu często nie dają żadnych wskazówek co do sposobów faktycznego użycia systemu. Program tcpdump obsługuje bogatą skład­ nię precyzyjnego określania pakietów sieciowych, które chce się poddać badaniu. Jed­ nak bez dostępu do faktycznych przykładów użycia trudno zrozumieć cele, którym służy kod źródłowy: przypadki testowe oferują materiał umożliwiający uruchomienie kodu źródłowego „na sucho”. Na szczęście jego strony podręcznikowe — podobne do innych uniksowych podręczników — prezentują dziesięć różnych typowych scenariu­ szy użycia, takich jak poniższy27 2S. W celu wyświetlenia całości ruchu ftp odbywającego się przez bramę internetową snup (należy zwrócić uwagę, że wyrażenie umieszczono w apostrofach w celu zapobieżenia błędnej interpretacji nawiasów przez powłokę systemową): tcpdump 'gateway snup and (port ftp or ftp-data)'

W celu wyświetlenia pakietów początkowych i końcowych (pakietów SYN i FIN) każdej konwersacji TCP, która dotyczy maszyny nie należącej do sieci lokalnej: tcpdump 'tcp[13] & 3 !- 0 and not src and dst net localnet'

yy

hsqldb/dev-docs/hsqldb/index.html.

-3perl/pod/perlguts.pod. 24 doc/perlguts.pdf. 25 netbsdsrc/share/man/man9/mbuf.9. 2h doc/mbuf.pdf. 27

*)o

netbsdsrc/usr. sbin/tcpdump/tcpdump. 8. doc/tcpdump.pdf. s. 6.

Rozdział 8. ♦ Dokumentacja

P E R L G U T S t 1)

237

U ser C ontributed Perl D ocum entation

P E R L G lT S t 11

NAME p erlguts - P e rl's Internal F unctions DESCR IPTION T ilts docum ent attem pts to d escrib e som e o f the internal function s o f the Perl ex ecu tab le It is far from co m p lete and p robably c o n tain s m any errors. P lease re fer any q u estio n s o r com m ents to the au th o r below Variables

Datatypes Perl bus three typedefs that h an d le P e rl's th re e m ain d ata types:

SV AV HV

Scalar Value Array Value Ha sh V a l u e

E ach ty p e d ef has specific ro u tin es that m anipulate the various data types

W hat Is an “ IV"? Perl uses a special ty p e d ef IV w h ich is a sim ple integer ty p e that is g uaranteed to be large enough to hold a p o in ter (as w ell a s an integer). Perl :dso uses tw o special typedefs. 1.12 and 116. w hich w ill alw ay s he at least 32-hits an d 16-hils long respectively.

Working with SVs A n SV can be crea ted and loaded w ith o n e com m and. T h e re are fo u r ty p es of values that can be loaded: an integer value (IV), a d o u b le (N Vt, a string. (PV), an d an o th er scalar (SV). T h e six routines are:

SV* SV* SV* SV* SV* SV*

ne wS Vi v( IV ); newSVnv(double); ne wS Vp v ( c h a r * , int); ne wS Vp v n ( c h a r * , int); n e w S V p v f ( c o n s t char*, newSVsv(SV*);

...);

T o c h a n g e th e value o f an " alread y -ex istin g “ SV. there arc seven rou tin es.

vo id void vo i d vo id vo i d vo id vo i d vo i d

s v _s et iv (S V* , IV); s v _s et uv (S V* , U V ) ; sv _s et nv (S V* , double); s v _s et pv (S V* , c o n s t char*); s v _ s e t p v n ( S V * , c o ns t char*, int) s v _s et pv f( SV *, c o ns t char*, ...); s v _ s e t p v f n ( S V * , c o n s t char*, STRLEN, va _l is t *, SV **, 132, bool); s v _s et sv (S V* , SV*);

N otice that you can ch o o se to specify the length o f the string to be assigned by u sin g 5V_setpvn. newSVpvn, o r newSVpv. o r you m ay allow Perl to ca lcu la te the length by u sin g s v _ s e t p v o r b y sp eci­ fying 0 us the seco n d arg u m en t to newSVpv. B e w arned, thoug h , that Perl w ill determ in e the strin g 's length by using s t r l en. w hich d epends o n the string term inating w ith a NUL character. T he argum ents o f s v _ s e t p v f are p rocessed like spri ntf. and the form atted ou tp u t beco m es the I.tine

s v _ s e t p v f n is an an alo g u e o f vsprintf. but it allow s you to specify eith er a po in ter to a variable argum ent list o r th e ad d ress and length o f an array o f S V s T h e last argum ent po in ts to a b oolean: on return, if that boolean is true, then locale-specific inform ation has been used to form al the string, and the s trin g 's co n ten ts are th erefore untrustw orthy (see (he p e r/se c m anpage). This po in ter m ay he NULL if that 2/M ay/1 WO

peri 5.0 0 5 . p atch 0 3

Rysunek 8.2. Pierwsza strona podręcznika perlguts

I

238

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Dokumentacja często opisuje znane problemy i błędy implementacyjne. Czasem można spędzić całe godziny nad określonym fragmentem kodu źródłowego próbując zrozumieć, w jaki sposób (mimo mylących pozorów) obsługuje on poprawnie określone dane wej­ ściowe. Czytając dokumentację można niejednokrotnie dowiedzieć się, że określona sytuacja, którą badamy, w rzeczywistości nie jest obsługiwana przez system i opisuje się to jako błąd. Uniksowe strony podręcznikowe często zawierają sekcję o nazwie „Bugs” (Błędy), w której dokumentuje się właśnie takie przypadki. W rzeczywistości w sekcji tej można znaleźć komentarze dotyczące ograniczeń w zakresie projektu im­ plementacji lub interfejsu29' 30: W chwili obecnej implementacje programów at i batch nie są odpowiednie, kiedy użytkownicy rywalizują o zasoby. Jeżeli taka sytuacja ma miejsce, warto rozważyć wykorzystanie innego systemu przetwarzania wsadowego, takiego jak nqs. podpowiedzi odnośnie do użycia programu31 32: Ze względu na działanie mechanizmu obsługi języka powłoki używanego w celu przekierowywania danych wyjściowych polecenie „cat fi le i fi le2 > fi l e i ” spowoduje zniszczenie oryginalnych danych znajdujących się w pliku fi le i. Jest to wykonywane przez powłokę zanim zostanie uruchomione polecenie cat. akcenty humorystyczne33,34: Nie istnieje specyfikacja konwersji dla faz księżyca. jak również opisy faktycznych błędów35' 35: Rozpoznawanie funkcji, podprogram ów i procedur języków FORTRAN i Pascal jest bardzo uproszczone. Nie są podejmowane żadne próby obsługi struktury blokowej: jeżeli w dwóch różnych blokach występują dwie procedury Pascala o tej samej nazwie, pojawia się błąd. Niedoróbki i oczywiste błędy występujące w środowisku rozwojowym lub uruchomie­ niowym mogą stanowić poważne źródło problemów. Są one niekiedy dokumentowane przez producentów w systemach obsługi klienta lub w listach poprawek dołączanych do łat programowych — nie są to jednak najpopularniejsze źródła szukania informacji w przypadku, gdy program zacznie się zachowywać w nieoczekiw'any sposób. Na szczę­ ście znane wady środowisk są zazwyczaj dokumentowane w kodzie źródłowym. Kod neibsdsrc/usr. binJat/ai. I. 30 doc/at.pdf. 31 netbsdsrc/bin/cat/cat. 1. 32 doc/cat.pdf. 33

netbsdsrc/lib/libc/time/strftime. 3.

34 dodstrftime.pdf. 35 netbsdsrc/usr.bin/ciags/clags. 1. 30 docictags.pdf.

Rozdział 8. ♦ Dokumentacja

239

to oczywiste miejsce, w którym programista może nieco rozładować frustrację spowo­ dowaną przez problem poprzez skarcenie lub zaatakowanie odpowiedzialnego za to producenta37. // The following function 1s not Inline, to avoid build (template // instantiation) problems with Sun C++ 4.2 patch L04631-07/SunOS 5.6

A

Dokumentacja zmian może pomóc w lokalizowaniu problemów. K.od źródłowy wielu systemów zawiera pewien rodzaj dokumentacji konserwacyjnej. W najgorszym razie jest to opis zmian dokonanych po każdej publikacji — w formie wpisów z rejestru sys­ temu kontroli wersji, jakie opisano w podrozdziale 6.5, lub zwykłego pliku tekstowego, zwykle noszącego nazwę ChangeLog, w którym komentarze dotyczące wprowadzanych zmian są ułożone w porządku chronologicznym. Przejrzenie rejestru zmian często po­ zwala odkryć fragmenty, w których są dokonywane częste, czasem sprzeczne zmiany lub gdzie podobne poprawki są wprowadzane w różnych częściach kodu źródłowego. Pierwszy rodzaj zmian może niekiedy wskazywać na występowanie fundamentalnych błędów projektowych, które osoby konserwujące kod starają się naprawić szeregiem poprawek. Poniższe wpisy z pliku ChangeLog dla kodu narzędzia smbfs (sieciowego systemu plików Windows) systemu Linux stanowią tu adekwatny przykład. 2001-09-17 Urban [...] * proc.c: Go back to the interruptlble sleep as reconnects seem to handle 1t now.

[...] 2001-07-09 Jochen [...] * proc.c. loctl.c. Allow smbmount to signal failure to reconnect with a NULL argument to SMB IOC NEWCONN (speeds up error detection)

[...] 2001-04-21 Urban [...] * dlr.c. proc.c: replace tests on conn_p1d with tests on state to fix smbmount reconnect on smb_retry timeout and up the timeout to 30s.

[...] 2000-08-14 Urban [...] * proc.c: don't do lnterruptable_sleep 1n smb_retry to avoid signal problem/race.

[...] 1999-11-16 Andrew [ ..] * proc.c: don't sleep every time with win95 on a FINDNEXT

Jest rzeczą oczywistą, że kod w pliku proc.c musi się borykać z subtelnymi problemami czasowymi oraz występowaniem warunków wyścigu. Na usprawiedliwienie twórców narzędzia smbfs należy stwierdzić, że odpowiedni protokół komunikacji przez długi czas był nieudokumentowany i jest bardzo skomplikowany, co powoduje, że różne implementacje zachowują się w odmienny i niezgodny ze sobą sposób. Podobne poprawki zastosowane względem różnych części kodu źródłowego wskazują na występowanie łatwego do popełnienia błędu lub przeoczenia, które z dużą dozą prawdopodobieństwa mogą występować również w innych miejscach. Tego rodzaju zmiany ilustrują poniższe wpisy dotyczące kodu źródłowego edytora vi. 1.65 -> 1.66 (05/18/96) + Send the appropriate Tl/TE sequence 1n the curses screen whenever entering ex/vl mode. This means that :shell now

37ace/T4 0/lao/Sequence_ T. cpp: 130 - 131.

240

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source shows the correct screen when using xterm alternate screens

[...] 1.63 -> 1.64 (05/08/961 * Fix bug where TI/TE still weren't working - I didn't put in the translation strings for 8SD style curses.

[...] 1.62 -> 1.63 (04/29/96) + Fix bug where nvi under BSD style curses wasn't sending TI/TE terrocap strings when suspending the process.

A

Powyższe wpisy wskazują, że we wszystkich przypadkach, gdy edytor vi rozpoczyna lub kończy wykonywanie działań związanych z wierszem poleceń, odpowiednie polecenią adresowania kursora (TI/TE) muszą zostać przesłane na terminal. Kiedy badając kod spotka się takie wpisy, należy pomyśleć o podobnych przypadkach, które mogły zostać pominięte. W powyższym przypadku należałoby pomyśleć o innych możliwych poleceniach edytora, które działają w trybie wiersza poleceń (takich jak kompilowanie w środowisku edytora) i zbadać odpowiednie funkcje, szukając kodu. który w popraw­ ny sposób obsługuje zmiany trybu ekranu. Ćwiczenie 8.3. Przedstaw ogólną charakterystykę organizacji kodu źródłowego serwera WWW apache poprzez zapoznanie się z dostępną dokumentacją. Ćwiczenie 8.4. W jaki sposób sformalizowana struktura uniksowych stron podręczniko­ wych może zostać wykorzystana do zautomatyzowania procesu konstruowania prostych przypadków testowych? Ćwiczenie 8.5. Odwzoruj spis treści opisu systemu BSD Unix [ L M K Q 8 8 ] na dwa naj­ wyższe poziomy drzewa kodu źródłowego dostępnego na płycie dołączonej do książki. Ćwiczenie 8.6. Zlokalizuj na płycie dołączonej do książki jedno wystąpienie odwoła­ nia do opublikowanego algorytmu. Porównaj opublikowaną wersję algorytmu z jego implementacją. Ćwiczenie 8.7. Zidentyfikuj dwa przejścia stanów w implementacji protokołu TCP z płyty dołączonej do książki oraz odpowiednie zmiany na diagramie zmian stanów protokołu TCP38. Ćwiczenie 8.8. Zlokalizuj na płycie dołączonej do książki trzy przykłady użycia udoku­ mentowanych wewnętrznych interfejsów projektu Perl. Ćwiczenie 8.9. Podziel na kategorie i przedstaw w tabeli rodzaje problemów opisywa­ nych w sekcji „Bugs” uniksowych stron podręcznikowych i posortuj je według częstości występowania. Omów otrzymane wyniki. Ćwiczenie 8.10. Wyszukaj słowa „buffer overflow” w rejestrze zmian systemu kontroli wersji dużej aplikacji. Narysuj wykres zmian ich występowania w czasie. Ćwiczenie 8.11. Opracuj prosty proces lub narzędzie służące do wizualizacji częstotli­ wości zmian dotyczących określonych plików lub fragmentów plików używając bazy danych systemu kontroli wersji.

38 doc/rfc793.txt: s. 21.

Rozdział 8. ♦ Dokumentacja

241

8.3. Problemy dotyczące dokumentacji A Czytając dokumentację należy pamiętać, że często może ona dawać błędne wyobraże­ nie o kodzie źródłowym. Dwa różne przypadki błędnej interpretacji, z jakimi można się spotkać to nieudokumentowane funkcje oraz wyidealizowana prezentacja. Funkcje zaimplementowane w kodzie źródłowym mogą zostać celowo pominięte w dokumentacji z wielu różnych powodów, z których część ma charakter obronny. Dana funkcja może zostać nieudokumentowana, gdyż: ♦ nie jest oficjalnie obsługiwana; ♦ stanowi jedynie mechanizm pomocniczy dla odpowiednio przeszkolonych specjalistów; ♦ ma charakter eksperymentalny lub jest przewidziana do użycia w przyszłości; ♦ jest używana przez producenta w celu uzyskania korzyści w stosunku do konkurencji; ♦ została niepoprawnie zaimplementowana; ♦ stanowi zagrożenie dla systemu zabezpieczeń; ♦ jest przewidziana do użytku tylko przez część użytkowników lub tylko w niektórych wersjach produktu; ♦ jest koniem trojańskim, bombą czasową lub tylnym wejściem. Funkcje pomija się również czasem w dokumentacji z powodu zwykłego przeoczenia, szczególnie wówczas, gdy zostały one dodane w późniejszym okresie cyklu rozwojo­ wego. Przykładowe, przełącznik -C pozwalający na wyspecyfikowanie pliku konfigu­ racyjnego nie został udokumentowany na stronie podręcznikowej polecenia apropos^’40. Stanowi to prawdopodobnie przykład przeoczenia. Czytając kod należy być świado­ mym możliwości występowania nieudokumentowanych funkcji. Każdy taki przypadek należy zaklasyfikować jako uzasadniony, wynikający z nieuwagi lub o wrogim charakte­ rze i zgodnie z tym zdecydować, czy kod lub dokumentacja powinny zostać poprawione. Kolejnym grzechem popełnianym w przypadku opisywania w dokumentacji kodu źró­ dłowego jest wyidealizowana prezentacja systemu. W takim przypadku dokumentacja nie opisuje systemu zgodnie z jego faktyczną implementacją, lecz według kryteriów, które powinny być lub dopiero będą zaimplementowane. Zamierzenia autorów takich dokumentów są często jak najbardziej szczere — piszą oni dokumentację użytkownika bazując na specyfikacji funkcjonalnej, wierząc, że system zostanie zaimplementowa­ ny według jej wytycznych. W innych przypadkach dokumentacja projektu architektury systemu bywa nie aktualizowana w czasie intensywnego wprowadzania zmian struk­ turalnych tuż przed publikacją oprogramowania. W obu przypadkach czytelnik kodu powinien zacząć patrzeć na dokumentację z większym krytycyzmem. 39 netbsdsrc/usr.bin/apropos/apropos. I. 40 doc/apropos.pdf.

242

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Niekiedy dokumentacja bywa również niespójna w kontekście samej siebie, co wyni­ ka z faktu, że nie da się jej w prosty sposób zweryfikować przy użyciu kompilatora lub testów regresyjnych. Przykładowo, strona podręcznikowa polecenia la st systemu NetBSD zawiera opcję -T w opisie polecenia, ale nie zawarto jej w opisie składni41'4*. Ponownie, duża liczba tego rodzaju problemów (nie jest tak w opisanym powyżej przy­ padku) może stanowić sygnał, że dokumentacja została napisana lub skorygowana w sposób niestaranny i należy ją czytać z pewnym dystansem. Istnieją również przypadki, w których dokumentacja wykorzystuje (mówiąc oględnie) bardzo idiomatyczny język. Na płycie dołączonej do książki można znaleźć około 40 wystąpień wyrazu grok. Jeżeli czytelnik go nie zna. nie wynika to z nieznajomości współ­ czesnej angielszczyzny. Wyraz grok występuje w powieści science-fiction Roberta A. Heinleina Stranger in a Strange Land [Hei61] i oznacza marsjańskie słowo: dosłownie „pić”, a w przenośni — „być jednym z”. W dokumentacji kodu źródłowego wyraz ten oznacza zwykle „rozumieć”43. // For linkers that cant grok long names. #deflne ACE_Cleanup_Strategy ACLE

W razie napotkania problemów ze zrozumieniem dokumentacji zawierającej nieznane wyrazy można spróbować odszukać je w słowniczku dokumentacji (o ile taki istnieje), w pozycji The New Hacker's Dictionary [Ray96] lub za pomocą wyszukiwarki inter­ netowej. Ćwiczenie 8.12. Płyta dołączona do książki zawiera ponad 40 odwołań do nieudoku­ mentowanych funkcji. Spróbuj je zlokalizować i omów najczęściej występujące powody nieścisłości.

8.4. Dodatkowe źródła dokumentacji Szukając dokumentacji kodu, warto w'ziąć pod uwagę również mniej popularne źródła, takie jak komentarze, normy, publikacje, przypadki testowe, listy dyskusyjne, grupy dyskusyjne, rejestry zmian, bazy danych zagadnień, materiały marketingowe oraz sam kod źródłowy. W przypadku badania dużego fragmentu kodu normalną rzeczą jest po­ mijanie dokumentacji zawartej w komentarzach, kiedy szuka się bardziej formalnych źródeł, takich jak dokumentacja wymagań lub projektowa. Jednak komentarze zawarte w kodzie źródłowym są często lepiej konserwowane niż odpowiednie dokumenty for­ malne i często są źródłem bardzo ciekawych informacji, na przykład w formie rysun­ ków ASCII. Przykładow'0 , diagramy z rysunku 8.3 oraz formalny dowód z listingu 8.144 to fragmenty komentarzy zawartych w kodzie źródłowym. Diagramy ASCII obrazują

41 nethsdsrc/usr.bin/last/last. 1. 4" doc/tast.pdf. 43 ace/ace/Cleanup_Strategies_T.h: 24 - 25. 44 netbsdsrc/sys/kern/kem_synch.c: 102- 135.

243

Rozdział 8. ♦ Dokumentacja

i«ix«d m ■1e tn .......... AU* 1 In

Nic

en

Ut pratendtd to Dt i DIE for allocating left-. 'f It turns out that *t art tn reality performing at a OCE ut need to reshuffle the leps.

6A:

Lin # 1n

i i a

Lin# •

t M a t e n - 11 I

header_only ||

ap_table get(r->headers_out. "Content-Length") || *-

[...]

Odczyt : tablicy wartości "Content-Length "

[...] Zapis na tablicy wartości "Keep-Alive"

Istotnym elementem architektury rozproszonej jest stosowany protokół komunikacji. Omówione dotąd protokoły typu klient-serwer oraz tablicy stanowiły rozwiązania doraźne zoptymalizowane dla określonych zastosowań. Bardziej uniwersalne podejście polega na wykorzystaniu abstrakcji zdalnego wywoływania procedur (ang. remote procedurę cali) lub zdalnego wywoływania metod (ang. remote method invocation). Pozwala to, aby kod klienta wywoływał procedurę na zdalnym serwerze i otrzymywał wyniki. Zgod­ nie z oczekiwaniami wymaga to wysokiego poziomu koordynacji klienta z serwerem, jak również dużego wsparcia ze strony infrastruktury określanej mianem warstwy po­ średniej (ang. middleware). Poniżej opisano najczęściej stosowane architektury warstwy pośredniej. ♦ CORBA (Common Object Request Broker Architecture) to niezależna od architektury i języka specyfikacja, określająca transparentną komunikację między aplikacjami i obiektami aplikacyjnymi. Została zdefiniowana i jest obsługiwana przez niedochodowe zrzeszenie ponad 700 firm programistycznych, producentów i użytkowników — Object Management Group (OMD). W systemach bazujących na standardzie CORBA pośrednik żądania obiektu (ang. object request broker, ORB) negocjuje przesłanie żądania klienta do odpowiedniej implementacji obiektu. ♦ DCOM (Distributed Component Object Model) to architektura obiektowa zaprojektowana w celu promowania możliwości współdziałania obiektów programowych w rozproszonych, heterogenicznych środowiskach. Została zdefiniowana przez firmę Microsoft i jest związana głównie z systemami Windows. Jej rozwinięcie stanowi architektura platformy .NET. ♦ RMI (Remote Method Invocation) to technologia specyficzna dla języka Java, która zapewnia płynne zdalne wywoływanie metod obiektów w różnych maszynach wirtualnych Java. ♦ RPC (Remote Procedurę Cali) firmy Sun to niezależny od architektury i języka protokół przesyłania komunikatów, którego firma Sun Microsystems i inne używają w celu zdalnego wywołania procedur w ramach różnych infrastruktur sieciowych. Na podbudowie specyfikacji RPC utworzono wiele protokołów dziedzinowych o wyższym poziomie, takich jak bazy danych konfiguracji rozproszonych hostów NIS (Network Information System) oraz NFS (Network File System). Programy wykorzystujące jeden z obiektowych systemów warstwy pośredniej (CORBA, DCOM lub RMI) definiują metody po stronie serwera w klasach rozszerzających od­ powiednie klasy podstawowe z warstwy pośredniej. Po stronie klienta inna klasa jest

258

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

A m

odpowiedzialna za zlokalizowanie serwera w sieci i zapewnienie odpowiednich metod pośredniczących (ang. stubs) w celu przekierowywania wywołań do serwera. W przypadku RPC interfejs API pozwala serwerowi na rejestrowanie procedur RPC, zaś klien­ towi na wywoływanie procedur serwera (Listing 9.214' l5, l6). Wszystkie omówione syste­ my oprócz RMI muszą radzić sobie z różnymi reprezentacjami danych w architekturach różnych procesorów. Problem ten rozwiązuje się poprzez uszeregowanie (ang. marshal­ ling) typów danych do postaci formatu niezależnego od architektury używanego w celach komunikacji między dowolnymi węzłami. Uszeregowywanie może występować albo niejawnie (CORBA, DCOM) poprzez opisanie interfejsu w języku definicji interfejsów (ang. interface definition language, IDL). który jest kompilowany do postaci odpo­ wiedniego uszeregowanego kodu. albo jawnie (Sun RPC) poprzez wywoływanie funk­ cji transformacji danych.

Listing 9.2. Zdalne wywoływanie procedur w implementacjiyp/NIS___________________________ ma1n(1nt argc. Char * a r g v [])

• — Strona serwera

( [...] 1f (!svc_register(transp. YPPROG. YPVERS. ypprog_2. proto)) { • -------- Rejestracja usługi _msgóut("unable to register (YPPROG. YPVERS. udp).“): exit(l);

1

) [...]

________________________

s t a t ic vo id • ypprog_2( Struct svc_req *rqstp. SVCXPRT *transp)

(

Punkt wejściu po stronie serwera

[...] switch (rqstp->rq_proc) {

[•■■] case YPPROC_NEXT: .-M--------------------------------------------------------------------Ohsluga wwvlania xdr_argument - xdr_ypreq_key; />rof«A/n'YPPROCjEXT xdr result - xdr_ypresp_key_val: local - ypproc_next_2_svc: break.

[...) result - (*local HSargument. rqstp):• --------------------------------- Wywotamc ________________________________________ ^ procedury lokalnej if (result !- NULL && !svc_sendreply(transp. xdr result. r e s u l t f n 1— Zwrocemcmmku

sveerr systemerr(transp):

} [...] - Strona klienta r - clnt_call(ysd->d0n)_cllent. YPPROC_NEXT. • ----------- Zdalne u w a lanie xdr_ypreq_key. &yprk, xdr_ypresp_key_val. îyprkv. YPROC_NEXT _yplib_timeout); t>00l_t • -------------------------- I s:crcgtnianic danych xdr_ypdomain wrap str1ng(XDR *xdrs. char **objp)

( return xdr_strlng(xdrs. objp. YPMAXOOMAIN):

1

14 netbsdsrc/usr.sbin/ypserv/ypserv/ypsen’.c. 125 - 399. 15 netbsdsrc/lib/libc/yp/ypJirst.c: 166- 168. 16 netbsdsrc/lib/libc/yp/xdryp.c\ 130- 136.

259

Rozdział 9. ♦ Architektura

9.1.2. Architektura przepływu danych Kiedy przetwarzanie można modelować, projektować i implementować jako serię prze­ kształceń danych, często używa się architektury przepływu danych (potoków i filtrów). Weźmy pod uwagę tworzenie bazy danych zawierającej krótki opis każdego wpisu znaj­ dującego się w dokumentacji uniksowej. Na przykład wpis dla funkcji getopt (której strona podręcznikowa została przedstawiona w podrozdziale 2.3) będzie miał postać: getopt (3) - get optlon character frora command U n e argument tist

Zadanie to można modelować jako serię oddzielnych kroków: 1 . Znajdź wszystkie strony podręcznikowe. 2 . Usuń wpisy zdublowane (kiedy pojedyncza strona podręcznikowa dokumentuje

wiele poleceń, występuje jako łącze dla wielu nazw plików). 3. Wyodrębnij odpowiednie wpisy. 4. Połącz je w ramach bazy danych. 5. Zainstaluj bazę w jej docelowej lokalizacji. Każdy z tych etapów jest związany z pobraniem danych wejściowych z poprzedniego i utworzeniem danych wyjściowych przekazywanych następnemu. Chociaż architektura przepływu danych może wydawać się restrykcyjna, często oferuje wiele korzyści. Po pierwsze, taka architektura często modeluje rzeczywiste procesy i sposób, w jaki faktycznie odbywa się praca. Po drugie, realizacja systemu zaprojekto­ wanego przy użyciu architektury przepływu danych może zostać zaimplementowana albo sekwencyjnie, albo współbieżnie. Wreszcie, zaimplementowane przekształcenia danych mogą być z łatwością wielokrotnie używane. Sukces systemu Unix jako śro­ dowiska szybkiego tworzenia prototypów i działających aplikacji wynikał właśnie z powszechnego użycia jego wielu programów filtrujących. Architektura przepływu danych jest często stosowana w środowiskach automatycznego, wsadowego przetwa­ rzania danych, szczególnie na platformach, które w wydajny sposób obsługują narzę­ dzia przekształcania danych. Z drugiej strony architektura taka nie jest odpowiednia dla projektowania systemów reakcyjnych, takich jak frontony GUI lub aplikacje działające w czasie rzeczywistym. |T1 Sygnałem wskazującym na wykorzystanie architektury przepływu danych jest wykorzy­ stanie plików tymczasowych lub potoków w celu komunikowania się między różnymi procesami. Implementacja polecenia makewhatis, które tworzy bazę danych stron pod­ ręcznikowych przedstawioną na rysunku 9.1|7, stanowi tu typowy przykład (listing 9.3). Listing 9.3. Implementacja polecenia makewhatis bazująca na wykorzystaniufiltrów fin d SMANDIR \ ( -typ e f -0 -type 1 \ ) -name '* . [ 0 - 9 ] * ' -1 ? "j

sort -n |

*--------------Utworzenie listy stron

• --------------- Usunięcie duplikatów (ląay)

awk '(1f (u[$l]) next: u[$!]++ ; prlnt tli]' > tLIST

1' netbsdsrc/libexec/makewhatis/makewhatis.slr. 20 - 39.

260

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 9.1. Diagram przepływu danych tworzenia opisów stron podręcznikowych egrep ' V t l - W tusf | L xargs /usr/llbexec/getNAMt~j j— sed -e 's/ [a-zA-ZO-9]* \ \ - f - / * > STOP • -

■Znalezienie stron niesformatowanych - Wyodrębnienie nazwy wpisu - Sformatowanie jako wpisu do bazy danych

- Znalezienie stron sformatowanych egrep 'VOS' SllS^j *— -D la każdego pliku while read file • ----------------------do - Wyodrębnienie i sformatowanie wpisu sed -n -f /usr/share/man/raakewhatis sed Iflle: do bazv danveh done » STOP

egrep '\.[0].(gz|Z)S' SllS^f while read file • ---------do gzip -fdc Sf11e | 1— sed -n -f /usr/share/man/makewhatis.sed; done » STOP

“ Znalezienie skompresowanych stron sformatowanych - Dla każdego pliku - Odtworzenie meskompresowanej treści - Wyodrębnienie i sformatowanie wpisu do bazy danych

sort -u -o STOP STOP • --------------------------------------------------- Posortowanie bazy danych i usunięcie duplikatów Install -o bin -g bin -ni 444 STMP “SMANDIR/Whatls.db*' • — Instalacja w lokalizacji docelowej rm -f SL 1ST STMP +

Usunięcie plików tymczasowych

Najpierw potok tworzy plik zawierający listę wszystkich unikatowych stron podręcz­ nikowych zainstalowanych w systemie. Plik wynikowy zostaje następnie przetworzony przez trzy różne potoki — jeden dla każdego rodzaju znalezionych stron: niesformatowanych (kod źródłowy troff), sformatowanych (tekst ze znakami sterującymi) oraz skompresowanych sformatowanych. Wyniki wszystkich trzech potoków zostają wsta­ wione do jednego pliku, który z kolei zostaje przetworzony przez narzędzie sort w celu usunięcia pozostałych duplikatów wpisów (występujących na przykład w sytuacji, gdy strona podręcznikowa istnieje w postaci sformatowanej oraz niesformatowanej). W celu zrozumienia funkcjonowania systemu bazującego na architekturze przepływu danych, należy zrozumieć dwie rzeczy: jego procesy oraz dane, które są między nimi przesyłane. Procesy stanowią zwykle przekształcenia danych. Należy postarać się zro­ zumieć każde przekształcenie oddzielnie, analizując jego dane wejściowe i wyjściowe. Jeżeli procesy badanego systemu komunikują się przy użyciu potoków, często warto jest tymczasowo przekierować dane do pliku i zbadać jego zawartość. Narzędzie uniksowe tee pozwala zdefiniować takie odgałęzienie. Kiedy używane są pliki tymczasowe, należy po prostu wykluczyć polecenia usuwające je na zakończenie cyklu przetwarzania, W aplikacjach biznesowych dane często składają się z rekordów wierszowych, gdzie odrębne pola albo zajmują stalą liczbę znaków, albo są rozdzielone znakiem specjal­ nym. Jednak dane mogą również składać się z rekordów o stałej długości lub obiektów specyficznych dla aplikacji. Mogą również być zgodne z pewną konwencją przesyłania strumienia binarnego, jak ma to na przykład miejsce w przypadku narzędzia przetwa-

Rozdział 9. ♦ Architektura

261

rzania obrazów netpbm lub narzędzia edycji dźwięku sox. W celu zrozumienia struk­ tury systemu jako całości pomocne może okazać się narysowanie diagramu przepły­ wu danych (ang. data-flow diagram), takiego jak przedstawiony na rysunku 9.1. Jeśli będzie się miało szczęście, takie diagramy będą się znajdować w dokumentacji pro­ jektu systemu.

9.1.3. Struktury obiektowe Systemy wykorzystujące strukturę obiektową opierają swój projekt na współpracy obiektów, które lokalnie zachowują swój stan. Architektura systemu jest definiowana poprzez relacje istniejące między różnymi klasami lub obiektami oraz sposoby ich interakcji. Rzeczywiste systemy mogą się składać z setek różnych klas. Przykładowo, kod źródłowy szkieletu aplikacji publikującej kod XML Cocoon 18 zawiera ponad 500 klas języka Java. Określenie struktury takiego systemu może stanowić nie lada wyzwa­ nie. Na szczęście można wykorzystać standardową notację i słownictwo języka UML (Unified Modeling Language) w celu wyrażenia modelu systemu lub odczytywania jego istniejącej dokumentacji. Ponadto wiele narzędzi bazujących na języku UML pozwula na dokonywanie inżynierii wstecznej (ang. reverse engineering) najważniejszych ele­ mentów architektury i wyrażenie ich w notacji UML. Język UML jest używany w celu modelowania różnych perspektyw w c y k lu ż y c ia sys­ temu. Sięgają one od koncepcyjnych modeli rzeczywistości, przez abstrakcyjne modele specyfikacji programistycznych po konkretne modele faktycznych implementacji. Język UML, cierpiący na pewnego stopnia przeładowanie konstrukcjami notacyjnymi. oferuje dziewięć różnych diagramów, służących do wyrażania takich perspektyw. Badając kod faktycznego systemu, zwykle jego strukturę modeluje się przy użyciu diagramów klas lub obiektów, zaś jego wewnętrzne interakcje przy użyciu diagramów sekwencji lub kolaboracji. Podstawowymi elementami budulcowymi większości diagramów UML są klasy. Na rysunku 9.2 przedstawiono sposób reprezentacji klasy RequestF^lterValvel', w modelu UML. Trzy części prostokąta zawierają nazwę klasy (ang. class name), jej atrybuty (ang. attributes) oraz jej operacje (ang. operations), czyli metody. Nazwa klasy została zapisana kursywą na oznaczenie tego, że jest ona abstrakcyjna (zdefiniowano ją przy użyciu słowa kluczowego a b s t r a c t , stąd nie może być bezpośrednio używana do tworzenia instancji obiektów). Atrybuty i operacje są opatrywane symbolem + w celu oznaczenia ich jako publicznych ( p u b lic ) , czyli takich, do których mają dostęp wszyst­ kie metody, symbolem - w celu oznaczenia ich jako prywatnych ( p r iv a te ) , czyli takich, do których dostęp mają jedynie metody danej klasy, oraz symbolem # w celu oznaczenia ich jako chronionych (p ro te c te d ), czyli takich, do których dostęp mają jedynie metody danej klasy i jej klas pochodnych. Poza tym składowe zdefiniowane jako statyczne ( S t a t ic ) — w kontekście klasy, a nie jej poszczególnych obiektów — zostają wyróż­ nione podkreśleniem. Składowe abstrakcyjne ( a b s t r a c t ) wyróżnia się kursywą. Nie należy dać się zmylić składni definicji poszczególnych składowych. Kiedy używa się języka UML w celu dokonania inżynierii wstecznej istniejącego systemu, składnia zwykle odzwierciedla język implementacji systemu. Modele, takie jak opisywany

18cocoon/src. jt4/catalina/src/share/org/apache/cataiinaJvalves/RequestFilterVaiveja\'a.

262

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 9.2.

______________________ RequestFillerValvo _____________________

Reprezentacja klasy w języku UML

«allow String = null

-static final into. Suing - ~org.apache.calalina.valves.RequeslFillerValvo/VO' #dsny: String = null_________________________________________________ +gelAllow(): String ♦setAllow(allow: String): void +getDeny(): String ♦setDenyfdeny: String): void +getlnfo(): String

+invoke(request:): void #precalculate(list: String): RED #process(property: String, request:): void

w niniejszym podrozdziale, mają zastosowanie w przypadku wszystkich języków, które obsługują obiekty, w tym C++, C#, Eiffel, Java, Perl, Python, Ruby, Smalltalk oraz Visual Basic .NET. Ilustrując architekturę systemu, często ukrywa się część opisu klas (atrybuty i operacje), dzięki czemu zyskuje się więcej miejsca na zilustrowanie związków istniejących między różnymi klasami. Diagram klas języka UML prezentuje zbiór klas, interfejsów, kolaboracji oraz związ­ ków. Może być przydatny w zrozumieniu strukturalnych aspektów architektury sys­ temu. Przykładowo, diagram z rysunku 9.3 ilustruje hierarchię klas pakietu valves20 w' kontenerze serwletów systemu Tomcat: abstrakcyjna klasa Val veBasejest używana jako podstawa pozostałych sześciu klas. Strzałki łączące klasy stanowią notację języka UML wyrażającą związek generalizacji (ang. generalization relationship) — w opisy­ wanym przypadku chodzi o dziedziczenie, czyli fakt, że specjalizowana klasa (na przy­ kład RemoteAddrValve) dzieli strukturę i zachowanie klasy ogólnej (RequestFilterValve). Zatem określony związek wyraża następujący fragment kodu Java21: public final class RemoteAddrValve extends RequestFilterValve {

Rysunek 9.3.

Proste zwiqzki generalizacji

Związki między klasami szybko stają się bardziej złożone. W językach obiektowych, które obsługują dziedziczenie wielokrotne (ang. multiple inheritance), takich jak C+t\ Eiffel i Perl, klasa specjalizowana może dziedziczyć po wielu klasach nadrzędnych. Ponadto istnieją przypadki, w których klasa nie dziedziczy funkcjonalności po innej klasie, ale wprowadza związek implementujący jeden lub większą liczbę określonych interfejsów (ang. interfaces). Jest tak w przypadku niektórych funkcji występujących w przykładzie omówionym w podrozdziale 9.1.4. Java bezpośrednio obsługuje takie związki poprzez występowanie słowa kluczowego interface oraz klasy definiowanej przy użyciu deklaracji implements. Takie przypadki można modelować poprzez użycie 20jt4/catalina/src/share/org/apache/catalina/valves. 21jt4/catalina/src/share/org/apache/catalina/valves/Remote.4ddrValve.java: 83 - 84.

Rozdział 9. ♦ Architektura

263

związku realizacji (ang. realization relationship) języka UML w celu wskazania, że klasyfikator znajdujący się przy zakończeniu strzałki określa ustalenie, że klasyfikator znajdujący się na początku strzałki będzie wykonywał określone funkcje. Na rysunku 9.4 przedstawiono strukturę klas zawierającą zarówno związki realizacji, jak i generali­ zacji. Model ten, ilustrujący część klas należących do pakietu connector serwera Tom­ cat22, zawiera dwa interfejsy (Response oraz jego wersję specjalizowaną HttpResponse) i cztery klasy. Klasy ResponseWrapper oraz ResponseBase implementują interfejs Respo •»nse, natomiast klasy HttpResponseWrapper oraz HttpResponseBase implementują inter­ fejs HttpResponse, jak również dziedziczą funkcjonalność po klasach, odpowiednio, ResponseWrapper i ResponseBase. Chociaż związki te na modelu wydają się oczywiste, rozpoznanie struktury bezpośrednio na podstawie definicji w kodzie źródłowym Java rozłożonym na sześć odrębnych plików i dwa katalogi mogłoby stanowić pewną trud­ ność. Stąd też warto używać diagramów w celu ilustrowania związków występujących w przypadku architektur obiektowych. Rysunek 9.4.

Związki realizacji i generalizacji

Rysowanie diagramów odręcznie lub na tablicy czy papierze pomaga w zrozumieniu struktury systemu, jednak korzyści można również odnieść dzięki wykorzystaniu różno­ rodnych narzędzi, które potrafią tworzyć modele poprzez inżynierię wsteczną istnieją­ cego kodu. Wszystkie opisane dotąd diagramy UML zostały wygenerowane przy użyciu narzędzia modelującego open-source ArgoUML2'. Często istnieje możliwość zaimporto­ wania kodu źródłowego do narzędzia modelującego w celu dokonania inżynierii wstecz­ nej architektury systemu. Użyta wersja narzędzia ArgoUML potrafi importować kod Java oraz identyfikować związki generalizacji i realizacji występujące między klasami. Komercyjne narzędzia modelujące, takie jak Rational Rose lub Together ControlCenter, obsługują import w przypadku większej liczby języków i potrafią wykrywać oraz wy­ świetlać dodatkowe związki, takie jak asocjacje. Narzędzia te oferują również możli­ wość przeprowadzania modelowania poprzez inżynierią w obie strony (ang. round-trip engineering modeling), gdzie zmiany dokonywane w kodzie są uwzględniane w modelu i odwrotnie. 22

* ji4/catalina/src/share/org/apache/catalina/connector. 23 http://www.argouml.org.

264

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

9.1.4. Architektury warstwowe Systemy posiadające wiele alternatywnych równorzędnych podsystemów są często organizowane według architektury warstwowej. W przypadku takiej architektury każda oddzielna warstwa reprezentuje dobrze zdefiniowany interfejs do jej wyższej warstwy i komunikuje się z warstwą niższą przy użyciu innego, aczkolwiek standardowego inter­ fejsu. W ramach takiej struktury każda warstwa może zostać zaimplementowana przy użyciu różnych technologii lub podejść i nie ma to wpływu na architektoniczną spój­ ność całości. Sieci komputerowe oraz systemy operacyjne zwykle są zgodne z takim podejściem: warstwa fizyczna sieci nie ma wpływu na format przekazywanych nią danych i odwrotnie. Aplikacje użytkownika nie muszą uwzględniać szczegółów budowy osprzętu sieciowego, za pomocą którego się komunikują. Architektury warstwowe są zwykle implementowane poprzez zestawienie komponentów programistycznych ze standardo­ wymi interfejsami. W pewnym sensie każda warstwa reprezentuje interfejs maszyny wirtualnej (ang. virtual machine interface) dla warstw znajdujących się nad nią. W wielu przypadkach taką architekturę można rozpoznać w wyniku napotkania specyfikacji lub dokumentacji takich warstw interfejsów. Istotną regułą związaną z architekturą warstwową jest to, że niższe warstwy nie mogą używać warstw wyższych — użycie (nie wywołanie, lecz właśnie użycie) odbywa się wyłącznie w kierunku w dół. Gdyby wszystko używało wszystkiego, w'arstwy nie mo­ głyby być z łatwością zastępowane lub abstrahowane. Przykładowo, implementacja sieciowej warstwy fizycznej (na przykład Ethernet lub SONET) nie może zawierać założeń dotyczących protokołów wyższego poziomu (na przykład TCP/IP), które będą na jej podbudowie używane. Gwarantuje to, że inne protokoły wyższego poziomu (na przykład UDP/IP lub NETBIOS) mogą być implementowane na podbudowie protokołu Ethernet bez konieczności modyfikowania implementacji tego ostatniego. Kolejnym przykładem jest kod generowany przez różne kompilatory Java, który działa na dowol­ nej standardowej maszynie wirtualnej. W przypadku badania architektury warstwowej reguła ta pozwala stwierdzić, że dana warstwa systemu widzi warstwy niższe jako abs­ trakcje i (o ile spełnia stawiane wymagania) nie musi się martwić o sposoby swojego użycia przez warstwy wyższe. W celu konkretnego zilustrowania dość złożonej architektury warstwowej, poniżej opi­ szemy, w jaki sposób proste żądanie fwrite występujące w programie C działającym w systemie operacyjnym NetBSD daje w efekcie wzorzec bitowy związany z systemem Linux na dysku SCSI podłączonym do kontrolera Adaptec. Na rysunku 9.5 przedsta­ wiono różne funkcje wywoływane w celu obsłużenia lego żądania, warstwę, którą repre­ zentuje każda z funkcji oraz rolę pełnioną przez warstwę. Kiedy program jest wykonywa­ ny jako proces użytkownika, funkcja fwrite24 zapewnia buforowanie przez bibliotekę stdio, natomiast funkcja systemowa write25 obsługuje standardowy interfejs POSIX wywołań systemowych. Kiedy wywołanie systemowe osiąga jądro, sekwencja sys_ write2'1— > vn_write27 — > V0P_WRITE28 dekoduje wywołanie systemowe i skierowuje je "4 netbsdsrc/lib/libc/stdio/fwrite.c: 56 - 59. 25 netbsdsrc/sys/kem/syscalls.master: 52 - 53. *6 netbsdsrc/sys/kem/sys_generic.c: 228 - 286. 27 netbsdsrc/sys/kern/vfs_vnops.c: 370 - 394. 28

netbsdsrc/sys/kern/vnnde_ifsrc: 9 8 -1 0 3 .

Rozdział 9. ♦ Architektura

Rysunek 9.5. O peracja zapisu p liku: o d p ro g ra m u użytkow nika do u rządzenia

265

Nazwa funkcji

Warstwa___________________ Rola_________________________________

fw rlte 4 w rite

Biblioteka std lo Funkcja systemowa

4 sys_wr1te 4 vn_write i V0P_WRITE * ext2fs w rite * bwrite i VOPSTRATEGY i u fs s tra te g y a VOPSTRATEGY a spec strategy ; sdstrategy

Punkt wejścia do jądra Wierzchołek vnode Przełącznik wierzchołka vnode System plików Bufor jądra Przełącznik wierzchołka vnode Blok Przełącznik wierzchołka vnode Przełącznik urządzenia Operacje asynchroniczne

4

|

Buforowanie pamięci użytkownika Interfejs POSIX

Granica pamięd użytkownika I jądra

Granica kodu synchronicznego i asynchronicznego

| fs c s ip i done t aha done t aha_f1n1sh_ccbs ta h a in t r t INTR

Interfejs SCSI Polecenia urządzenia Bloki sterujące polecenia Przerwanie urządzenia Przerwanie systemowe

|

Przetworzenie argumentów Dostosowanie do interfejsu wierzchołka vnode Wiele interfejsów Struktura danych Buforowanie pamięci jądra Wiele Interfejsów Bloki logiczne Wiele Interfejsów Wiele urządzeń Kolejki we-wy |

Kolejkowanie i powiadomionie procesu Przetworzenie wyniku Przetworzenie wyniku Obsługa okna komunikatu Przetworzenie przerwania

do odpowiedniej funkcji systemu plików ext2fs_write29 (w tym przypadku chodzi o linuksowy system plików ext2fs). Od tego miejsca sekwencja bwrite3" -> V0P_STRAT ■*EGY31 —> ufs_strategy32 zapisuje blok na odpowiedniej pozycji logicznej urządzenia, natomiast sekwencja VOP_STRATEGY33 -» spec_strategy34 -> sdstrategy3’ skierowuje żądanie dotyczące przetwarzania kolejkowego do określonego urządzenia (w opisywa­ nym przykładzie jest to dysk SCSI). Uzupełnienie żądania sprzętowego jest w naszym przypadku propagowane w górę od urządzenia do funkcji powiadomienia przy użyciu przerwań. Warstwy te nie podlegają bezpośredniej interakcji z warstwami wyższymi, ale są synchronizowane poprzez użycie interfejsu sleep/wakeup. Stąd przerwania urzą­ dzenia, sygnalizujące zakończenie wykonywania polecenia, najpierw są przetwarzane przez INTR 6, następnie propagowane do funkcji związanej z urządzeniem (aha intr37), do funkcji przetwarzającej bloki sterujące charakterystyczne dla protokołu SCSI (aha_ finish_ccbs38), do funkcji przetwarzającej wynik polecenia (aha_donew ) i w końcu do funkcji, która powiadamia oczekujący proces i kolejkuje nowe żądania (scsipi_done40). Interfejs warstwy może składać się albo z rodziny komplementarnych funkcji obsługu­ jących określone pojęcie, albo z wymiennych funkcji obsługujących różne implementa­ cje abstrakcyjnego interfejsu. Przykładowo, w opisywanym kodzie funkcja systemowa 29

netbsdsrc/sys/ttfs/ext2fs/ext2fsj,eadwrUe.c: 169 - 298.

30netbsdsrc/sys/kem/vfs_bio.c\ 297 —350. 31 netbsdsrc/sys/kem/vnode_ifsrc\ 228 - 230. 32 netbsdsrc/sys/ufs/ufs/ufs_ynops.c\ 1616- 1651. 33 netbsdsrc/sys/kern/vnode_if.src: 228 - 230. 34 netbsdsrc/sys/miscfs/speefs/spec_ynops.c: 479 - 489. 35netbsdsrc/sys/dev/scsipi/sd.c: 429 - 492. 36netbsdsrc/sys/arch/i386/isa/vector.s: 148 - 200. 37 netbsdsrc/sys/dev/ic/aha.c: 464 - 508. 38

netbsdsrc/sys/dev/ic/aha.c: 374 - 459.

39netbsdsrc/sys/dev/ic/aha.c: 836 - 921. 40netbsdsrc/sys/dev/scsipi/scsipi_base.c: 266 - 346.

266

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

write wraz z innymi funkcjami, takimi jak read, open, close, fork i exec, formuje część

warstwy interfejsu wywołań funkcji systemu operacyjnego (pojęcie). Podobnie, funkcja bwrite obsługuje interfejs bufora jądra 41 wraz z funkcjami bremfree, bufinit, bio_doread, bread, brelse, biowait oraz biodone. Z drugiej strony, funkcja ext2fs_write obsługuje konkretną implementację operacji zapisu w systemie plików (specyfikacja abstrakcyjnego interfejsu) równolegle z funkcjami msdosfs_write42, nfsspec_write43, union_write oraz ufsspec write45. Podobnie, funkcja aha_intr obsługuje przerwania związane z urzą­ dzeniem wraz z funkcjami ascjntr46, fxp_intr47, ncr_intr48, pcic_intr 9, px_intr5, sbdsp_intr51 i wieloma innymi. W prezentowanym przykładzie niektóre warstwy ist­ nieją tylko w celu zapewnienia multipleksacji oraz demultipleksacji interfejsów różnych |~n warstw, w zależności od określonego żądania. Taka funkcjonalność jest implementowana przy użyciu tablic wskaźników na funkcje, podobnych do przedstawionej poniżej'2. struct vnodeopv_entry_desc ext2fs_vnodeop_entries[] - { f &vop_default_desc. vn_defauTt_error }, { &vop_lookup_desc. ext2fs_lookup ). /* lookup */ { &vop create_desc. ext2fs_create }. / * create */

[...] " ( &vop_update_desc. ext2fs_update ). ( &vop_bwrite desc. vn_bwrite ).

/ * update */ / * bwrite * /

{ (struct vnodeop desc*)NULL. (int(*) _P((vo1d*)))NULL )

}: Okazuje się, że systemy zaimplementowane w językach obiektowych bezpośrednio wy­ rażają operacje związane z multipleksacją interfejsów warstw poprzez użycie wy­ wołań metod wirtualnych.

9.1.5. Hierarchie Centralnym elementem w przypadku zarówno budowania, jak i prób zrozumienia zło­ żonych architektur jest pojęcie dekompozycji hierarchicznej (ang. hierarchical decom­ position). Hierarchie są często używane w celu pokonania złożoności poprzez rozdziele­ nie poszczególnych obszarów zainteresowania w ustrukturyzowany i łatwy do śledzenia sposób. Dekompozycja hierarchiczna jest często ortogonalna względem innych archi­ tektonicznych aspektów systemu. Architektura może zostać zdefiniowana w sposób hie41 netbsdsrc/sys/kern/vfs_bio.c.

4~netbsdsrc/sys/msdosfs/msdosfs_vnops.c: 501. 43 netbsdsrc/sys/nfs/nfs_vnops.c: 3194. 44 netbsdsrc/sys/miscfs/union/union_vnops.c: 917. 45 netbsdsrc/sys/ufs/ufs/ufs_vnops.c: 1707. 46 netbsdsrc/sys/dev/tc/asc.c: 824. 47 netbsdsrc/sys/dev/pci/if_fxp.c: 908. 48 netbsdsrc/sys/dev/pci/ncr.c: 4240. 49 netbsdsrc/sys/dev/ic/i82365.c: 467. 50 netbsdsrc/sys/dev/tc/px.c: 153. 51 netbsdsrc/sys/dev/isa/sbdsp.c\ 1481. 52 netbsdsrc/sys/ufs/exl2fs/exl2fs_ynops.c\ 1389- 1433.

Rozdział 9. ♦ Architektura

267

rarchiczny lub jedna albo większa liczba odrębnych hierarchii może przecinać i przeni­ kać różne struktury architektoniczne. Istotną sprawą jest zauważenie faktu, że system może być zorganizowany wzdłuż wielu osi poprzez użycie różnych odrębnych modeli dekompozycji hierarchicznej. W ramach konkretnego przykładu wystarczy spojrzeć na warstwy przedstawione na rysunku 9.5 (strona 265) — bez wątpienia stanowi on dekompozycję hierarchiczną — i porównać je ze strukturą katalogów zawierających odpowiednie funkcje. Nie są one ze sobą zgodne, gdyż są związane z dwoma różnymi modelami hierarchicznymi — jeden dotyczy przepływu sterowania i danych w ramach systemu, zaś drugi — organizacji kodu źródłowego. Badając architekturę systemu, struktury hierarchiczne można odkryć na podstawie następujących przesłanek: ♦ rozkład kodu źródłowego w katalogach; ♦ graf statycznych lub dynamicznych wywołań procedur; ♦ namespace (w C++), package (w Ada, Java, Perl) lub nazwy identyfikatorów; ♦ dziedziczenie klas i interfejsów; ♦ nazewnictwo ustrukturyzowanych wpisów w tablicach; ♦ klasy wewnętrzne lub procedury zagnieżdżone; ♦ nawigacja w ramach heterogenicznych struktur danych i asocjacji obiektów. Należy być przygotowanym na konieczność zaakceptowania różnych (często konflik­ towych) metod dekompozycji hierarchicznej w ramach architektury jako prób spojrzenia na zagadnienie od różnych stron. Diagram drzewiasty pomaga w modelowaniu dowolnej hierarchii, jaką się napotka.

9.1.6. Przecinanie Wartościowym narzędziem koncepcyjnym służącym do wyprowadzania szczegółów dotyczących struktury programu jest przecinanie (ang. slicing). Nieformalnie rzecz ujmując, przecięcie programu (ang. program slice) można postrzegać jako te jego czę­ ści, które mogą wpływać na wartości zmiennych obliczane w określonym miejscu. Zatem kryterium przecięcia (ang. slicing criterion), czyli specyfikacja części, które tworzą przecięcie, stanowi zbiór zmiennych oraz lokalizacji w programie. Przykład praktycznego wykorzystania tej techniki przedstawiono na listingu 9.4 ', Oryginalny kod (listing 9.4:2) został przecięty pod względem zmiennej cha ret oraz miejsca w pro­ gramie oznaczonego jako listing 9.4:1 (przedstawiono to na listingu 9.4:3), a także pod względem zmiennej 1 inect oraz tego samego miejsca w programie (przedstawiono to na listingu 9.4:4). Wartość przecinania jako techniki analizy programów wynika z tego, że pozwala ono zebrać razem dane oraz zależności sterujące programem: przepływ danych oraz kolejność wykonywania instrukcji. Dzięki temu przecinanie pozwala na dokonanie restrukturyzacji ścisłej dekompozycji hierarchicznej dużego programu po­ przez zgrupowanie niesekwencyjnych zbiorów instrukcji. Poprzez przecięcie dużego 53 netbsdsrc/usr.bin/wc/wc.c: 202 - 225.

268

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

fragmentu kodu względem kluczowej zmiennej wynikowej można wyeliminować nie­ istotne informacje i w dalszej perspektywie dokonać inżynierii wstecznej odpowiednie­ go projektu lub algorytmu. Przecinanie daje również dostęp do miary zwartości (ang. cohesion) modułu, czyli stopnia powiązania między przetwarzanymi w jego ramach elementami. Wysoki poziom zwartości potwierdza poprawność początkowego projektu architektonicznego, który powiązał jego elementy. Niski poziom zwartości wskazuje, że istnieje możliwość przegrupowania elementów modułu, które tylko przypadkiem zostały powiązane. W celu określenia zwartości modułu można utworzyć serię przecięć modułu w oparciu o jego różne zmienne wyjściowe. Części wspólne tych przecięć wskazują na istnienie związków elementów przetwarzania: ich rozmiar stanowi miarę zwartości modułu. Korzystając z tego samego podejścia na poziomie wielu modułów, można również mierzyć sprzężenie (ang. coupling), czyli stopień wzajemnego powią­ zania modułów. Wysoki poziom sprzężenia wskazuje kod, który jest trudno zrozumieć i zmieniać. W takim przypadku można użyć przecięć w celu odkrycia, w jaki sposób są ze sobą powiązane różne moduły. Listing 9.4. Przykłady przecięć programu while ( d e n - read(fd. buf, MAXBSIZE)) > 0) { • — -/2 ] Oryginalny kod charct +- len; for (C - buf: len--: ++C) { if 0) { • — [3] Przecięcie względem zmiennej charct charct len;

i while ( d e n * read(fd. buf. MAXBSIZE)) > 0) { • — (4] Przecięcie względem zmiennej for (C - buf; len--: ++C) { if (isspace(*C)) ( if (*C — '\n') { ++11nect;

1 ł

11 nect

1

)

Ćwiczenie 9.1. Opisz, w jaki sposób można określić schemat bazy danych w używanym systemie relacyjnym. Opracuj proste narzędzie, które przekonwertuje taki schemat do postaci elegancko sformatowanego raportu. Ćwiczenie 9.2. Zmienne globalne zmniejszają czytelność kodu. Pod jakim względem różni się od nich podejście wykorzystujące tablice?

Rozdział 9. ♦ Architektura

269

Ćwiczenie 9.3. Niewiele projektów open-source wykorzystuje oprogramowanie pośred­ nie. Wyjaśnij dlaczego. Ćwiczenie 9.4. Zlokalizuj na płycie dołączonej do książki aplikacje oparte na architek­ turze przepływu danych. Jaka będzie Twoja strategia wyszukiwania? Ćwiczenie 9.5. Pobierz program GNU tar 54 i pokaż w postaci diagramu przepływu danych, w jaki sposób obsługuje on zdalne wykonywanie kopii bezpieczeństwa, kom­ presowanie oraz operacje wejścia-wyjścia o stałym bloku. Ćwiczenie 9.6. Użyj narzędzia modelowania w języku UML w celu dokonania inżynierii wstecznej struktury aplikacji obiektowej z płyty dołączonej do książki. Ćwiczenie 9.7. Określ, w jaki sposób warstwy sieciowe OSI odpowiadają faktycznej implementacji kodu sieciowego systemu NetBSD. Ćwiczenie 9.8. Czy technika przecinania pomaga w zrozumieniu programów obiek­ towych? Omów, w jaki sposób prawo Demeter55 (patrz podrozdział „Dalsza lektura”) wpływa na możliwość stosowania przecięć programów.

9.2. Modele sterowania Model sterowania sytemu opisuje, w jaki sposób jego podsystemy składowe współgrają ze sobą. Podobnie jak ma to miejsce w przypadku struktur systemów omówionych w po­ przednim podrozdziale, wiele systemów stosuje prosty, scentralizowany, jednowątkowy model sterowania typu wywołaj i wróć (ang. cali and return). Ponadto niektóre struktu­ ry systemów, które omówiliśmy, są niejawnie oparte na określonym modelu sterowa­ nia: w strukturze przepływu danych podsystemy są sterowane przepływem danych, zaś w przypadku struktury obiektowej sterowanie jest zwykle koordynowane poprzez wywo­ łania metod. Inne popularne, niebanalne modele sterowania mogą być sterowane zda­ rzeniami, bazować na wykorzystaniu menedżera systemowego lub być związane ze zmianami stanów.

9.2.1. Systemy sterowane zdarzeniami W przypadku wielu systemów decyzje sterujące są podejmowane w wyniku odpowiedzi na generowane zewnętrznie zdarzenia. Okazuje się, że systemy bazujące na architekturze sterowania zdarzeniami (ang. event-driven architecture) pokrywają pełny zakres możli­ wych abstrakcji programistycznych: od podprogramów obsługi w niskopoziomowym. sterowanym przerwaniami kodzie asemblera i szeregowania procesów po implementację

54http://www.gnu.org/software/tar. 55 Ujmując rzecz nieformalnie, prawo Demeter określa, że należy dążyć do minimalizacji liczby klas

znajomych (ang. acquaintance classes) każdej z definiowanych metod — przyp. tłum.

270

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

wysokopoziomowych struktur GUI oraz wyzwalaczy bazodanowych. Implementacja systemów sterowanych zdarzeniami może przyjąć jedną z wielu postaci. Zdarzenia można transmitować do zbioru procesów nasłuchujących36: for dnt 1 - 0: 1 < Ust. length; 1++) ((Contai nerlistener) 1ist[i]).contatnerEvent(event):

lub. co często ma miejsce w przypadku przerwań sprzętowych, można wywoływać pojedynczy podprogram obsługi. Ponadto pewne aplikacje posiadają strukturę zw iązaną z pętlą zdarzeń, która monitoruje zdarzenia i kieruje przetwarzaniem 5 . while (XtAppPendlng(appCtx)) XtAppProcessEventtappCtx. XtlMAU):

zaś w innych przypadkach są one zintegrowane w ramach struktury, w której podpro­ gramy przetwarzania zdarzeń mogą rejestrować obsługę określonych zdarzeń. Ilustruje to listing 9.558' 59- 60. Listing 9.5. Pośrednia rejestracja zdarzenia ijego obsługa wXt_____________________________ *help*Paned.manua 1Page.translations:#overn de \ • --------------------- Definicje skrótów [... ] klawiaturowych (Xman.ad) Ctrld GotoPage(Dlrectory) \n\ Ctrlp_11st.le_next) { 1f (p->p_stat — SRUN && (p->p_flag & P_INMEM) — 0) { pr1 • p->p_swtime + p->p_slpt1me - (p->p_nice - NZERO) * 8; 1f (pr1 > pprD { PP - P; pprl - pr1:

- Znajdź proces do wymiany

} }

)

1f (Cp - pp) “ NULL) { tsleep((caddr_t)&procO. PVM. "scheduler". 0); goto loop:

} if

(cnt.v_free_count > atop(USPACE)) ( •swapin(p); goto loop;

-B rakzadań do wykonania Dezaktywacja pozwalająca na działanie zadań sterowanych przerwaniami “ Istnieje przestrzeń, umożliwiająca przeniesienie procesu do pamięci

» (void) splhlghO: •VH_WAIT; (void) splOO; goto loop;

-B ra k dostępnej przestrzeni Oczekiwanie na utworzenie wolnej przestrzeni przez demona stronicowania

Opisywany system stanowi w rzeczywistości hybrydę dwóch modeli sterowania. Sys­ tem sterowany zdarzeniami koordynuje normalne działania, natomiast bardziej nieza­ wodny model menedżera procesów omówiony powyżej jest używany jako mechanizm rezerwowy w sytuacjach krytycznych. Wiele rzeczywistych systemów zawiera najlep­ sze elementy kilku architektur. W takim przypadku nie warto na próżno szukać ogól­ nego obrazu architektury — należy lokalizować, rozpoznawać i analizować wszystkie style architektoniczne jako oddzielne, aczkolwiek powiązane ze sobą struktury.

9.2.3. Przejścia stanów Model przejść stanów (ang. state transition model) stanowi zarządzanie przebiegiem sterowania systemu poprzez manipulowanie danymi: stanem systemu. Dane stanu okre­ ślają, gdzie ma zostać skierowane sterowanie przebiegiem, zaś zmiany danych stanu służą do przekierowywania celu wykonania. Systemy (częściej — podsystemy), które działają zgodnie z modelem przejść stanów, są zwykle modelowane i posiadają strukturę

275

Rozdział 9. ♦ Architektura

maszyny stanów (ang. stale machinę). Maszyna stanów jest definiowana przez skoń­ czony zbiór stanów, w jakich może się znaleźć, oraz reguły określające przetwarzanie i przechodzenie z jednego stanu do drugiego. Dwa stany specjalne, stan początkowy (ang. initial stale) oraz stan końcowy (ang.finał stale), określają, odpowiednio, punkt (T) początkowy maszyny oraz warunek zakończenia jej działania. Maszyny stanów są zwy­ kle implementowane jako pętle z instrukcjami switch. Instrukcja switch ulega rozga­ łęzieniu zgodnie ze stanem maszyny. Każda instrukcja case służy do przetworzenia określonego stanu, jego zmiany oraz zwrócenia sterowania do początku maszyny sta­ nów. Typowy przykład maszyny stanów przedstawiono na listingu 9.868. zaś odpo­ wiadający jej diagram UML przejść stanów widoczny jest na rysunku 9.6. Określona maszyna stanów jest używana do rozpoznawania nazw zmiennych powłoki. Mogą one składać się z pojedynczej litery (stan PS_VAR1), z liter i cyfr (stan PS_IDENT) lub wyłącz­ nie z cyfr (stan PS_NUMBER). Stan PS_SAW_HASH jest używany w celu umożliwienia spe­ cjalnej obsługi zmiennych, których nazwy rozpoczynają się od znaku #. Zmienna State jest używana w celu przechowywania stanu maszyny. Początkowo ma wartość PS_INI »»TIAL (na diagramie UML oznacza to wypełnione kółko), zaś funkcja kończy działanie, kiedy jej wartość wyniesie PS_END (na diagramie UML — wypełnione kółko zawarte w pustym kółku). Rysunek 9.6. Diagram UML przejść stanów dla maszyny stanów

Listing 9.8. Kod maszyny stanów static char * get_brace_var(XStnng *wsp. char *wp)

{

enum parse_state ( PS INITIAL. PS SAW_HASH. PSJDENT. PS NUMBER. PS VAR1. PS END

}

state: • -------------------------char c:

68 netbsdsrc/bin/ksh/lex.c: 1124 - 1194.

-Stany

- Zmienna stanu

276

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source State ■ PS INITIAL; • ---------------------------------------------- Stan początków

while (1) T c - getscO: /* State machine to figure out where the variable part ends. */ switch (state) { case PSJNITIAL:

[...] case PS_SAW_HASH: 1 f (letter(c))

• --------------------- Regutyprzejićstanów

state - PSJDENT: else if (digltTc)) state - PS_NUMBER; else if (ctype(c, C_VAR1)) state - PS_VARlT else state - PSEND: break:

[...] case PS_VAR1: S tate * PS_END: • ---------------------- Stan końcom' break:

}

if (State " PS END) { # ------------------- Zakończenie, kied\'zostanie asutgnięn'.stankońcom

[ - .] break:

ł [...] }

return wp;

} Maszyny stanów są używane w celu implementacji prostych systemów reakcyjnych (interfejsów użytkownika, sterownika termostatu lub silnika, aplikacji automatyzacji procesów), maszyn wirtualnych, interpreterów, programów dopasowujących wyrażenia regularne do wzorca oraz analizatorów leksykalnych. Diagram przejść stanów często pomaga w rozwikłaniu sposobu działania maszyny stanów. Ćwiczenie 9.9. Opisz, w jaki sposób obsługa komunikatów interfejsu API Microsoft Windows SDK zilustrowana na listingu 9.6 mogłaby zostać zorganizowana przy użyciu struktury obiektowej. Ćwiczenie 9.10. Prześledź obsługę zdarzeń systemu automatycznego zarządzania zasi­ laniem zawartego na płycie dołączonej do książki. Ćwiczenie 9.11. Zlokalizuj aplikacje bazujące na wątkach i opisz wykorzystywany przez nie model sterowania. Ćwiczenie 9.12. W jaki sposób są zwykle implementowane modele sterowania przejść stanów w kodzie znajdującym się na płycie dołączonej do książki? Zaproponuj sposób, w jaki kod takich modeli można by organizować, zachowując jego czytelność, w miarę wzrostu ich złożoności.

277

Rozdział 9. ♦ Architektura

9.3. Pakietowanie elementów Odpowiednikiem wstępującym zstępujących struktur architektonicznych jest sposób łączenia w pakiety poszczególnych elementów systemu. W przypadku dużego bloku kodu istotną rzeczą jest zrozumienie mechanizmów umożliwiających dokonanie jego dekompozycji na oddzielne jednostki.

9.3.1. Moduły Najczęściej spotykanym elementem dekompozycji jest zapewne moduł (ang. module): oddzielnie nazwany i adresowalny komponent, który oferuje innym modułom usługi. Chociaż przedstawiona definicja uwzględnia wiele różnych elementów dekompozycji, takich jak obiekty i filtry, należy pamiętać, że zostaną one opisane bardziej szczegó­ łowo oddzielnie, zaś termin moduł będzie stosowany jedynie wówczas, gdy nie będzie miał zastosowania żaden inny przypadek. Moduły zwykle podlegają interakcji z innymi modułami poprzez wywołania procedur oraz współużytkowanie danych. Fizycznym ograniczeniem modułu jest w większości przypadków pojedynczy plik, pliki zorgani­ zowane w ramach katalogu lub kolekcja plików o unikatowym przedrostku. Ponadto, w językach, które dopuszczają używanie zagnieżdżonych definicji procedur (na przy­ kład w Pascalu), moduły są implementowane jako procedury dostępne globalnie. Choć podobna metoda może być również użyta do zgrupowania wielu modułów w ramach jednego pliku przy użyciu funkcjonalności konstrukcji namespace języka C++ lub abs­ trakcji module języka Modula, w praktyce rzadko się tak postępuje. Jednakże mecha­ nizmy te, co zostanie opisane w podrozdziale 9.3.2, są często używane w celu grupo­ wania funkcji implementowanych w wielu plikach w ramach pojedynczego modułu. Typowym systemem zorganizowanym zgodnie z pojęciem równoważności pliku i mo­ dułu jest edytor systemu BSD «V/6 . Każdy ze 113 składających się na niego plików definiuje jedną lub więcej funkcji publicznych lub elementów danych obsługiwanych przez pewne funkcje prywatne (s ta tic ) — patrz listing 9.970. Listing 9.9. Moduł edytora nvi________________________________________________________ Static Char * const fmt[] " ( • ------------------------------------ Dane i defmicje lokalne «efine DEC 0

■tier. #deflne SDEC "X+ld".

1

[...] ): /*

• — Funkcja puhliczna

* vjncreroent -- [count]#[#+-] *

*

Increment/decrement a keyword number

* PUBLIC- int v increment _P((SCR *. V1CMD *)):

*/

int

69 netbsdsre/usr. bin/vi. 70nelbsdsrc/usr.bin/viA>i/v_increment.c: 31 -267.

278

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source v incrementtSCR *sp. V1CMD *vp)

c

c...]

) S ta tic void

• -------------------------- Funkcja lokalna

inc_err(SCR *sp. enum nresult nret)

{

I...]

) IT! Pakiet systemu BSD window 71 (tekstowy interfejs okienkowy) jest systemem, który wykorzystuje konwencję przedrostkowego nazewnictwa plików w celu konstruowania modułów z grup plików. Poniżej przedstawiono niektóre moduły i odpowiadające im nazwy plików. ♦ Keyboard command handling (obsługa poleceń klawiaturowych): cmd.c, cm dl.c, cmd2.c, crndS.c, and4.c, cmdS.c, cmdó.c, cmd7.c. ♦ Command-line processing (przetwarzanie w wierszu poleceń): Icmd.c, Icmd.h, Icmdl.c, lcmd2.c. ♦ Parser (analizator składniowy): parser.h, parser l.c, parser2.c, parser3.c, parser4.c, parser5. c. ♦ Term inal driver (sterownik terminalu): tt.h, ttflOO.c, ligeneric.c, nhl9.c, tth29.c, ttinit.c, ttoutput.c, tttermcap.c, ttt\ń925.c, ttwyseóO.c, ttwyse75.c, Itzapple.c, tizentec.c. ♦ Window system (system okienkowy): ww.h, wwadd.h, wwalloc.c, wwbox.c, wwchild.c, wwclose.c, wwclreol.c, wwclreos.c, wwcursor.c, wwdata.c, wwdelchar.c, wwdelete.c, wwdelline.c, wwdump.c, wwend.c, wwenviron.c, wwerror.c, wwflush.c, wwframe.c, wwgets.c, wwinit.c, wwinschar.c, wwinsline.c. wwiomux.c, wwlabel.c, wwmisc.c, wwmove.c, wwopen.c, wwprintf.c. wwpty.c, wwputc.c, wwpnts.c, wwredraw.c, wwredrawwin.c, wwrint.c, wwscroll.c, wwsize.c, wwspawn.c, wwsuspend.c, wwterminfo.c, wwtty.c, wwunframe.c, wwupdate.c, wwwrile.c. A zatem 75 plików należących do systemu zostało zgrupowanych w ramach pięciu róż­ nych modułów. Pozostałe 25 plików zapewne reprezentuje odrębne moduły, w wielu przypadkach zgrupowane jako plik nagłówkowy zapewniający dostęp do publicznego interfejsu oraz plik języka C oferujący odpowiednią implementację (char.h/char.c. string.hlstring.c, var.hlvar.c, xx.hlxx.c). Warto w tym miejscu zauważyć, że chociaż nazwy plików systemu okienkowego oraz sterownika terminalu są rozsądne, konwencja nazewnictwa użyta w przypadku obsługi poleceń klawiaturowych, przetwarzania w wier­ szu poleceń oraz analizatora składniowego pozostawia wiele miejsca na ulepszenia. Wreszcie, jądro systemu BSD : stanowi przykład systemu, który grupuje moduły w od­ rębnych katalogach. W tabeli 9.1. przedstawiono pewne przykłady tych katalogów. Nale­ ży jednak zauważyć, że niektóre obszary funkcjonalności są dzielone dalej poprzez 71

netbsdsrc/usr. bin/window.

7" netbsdsrc/sys.

Rozdział 9. ♦ Architektura

279

Tabela 9.1. Katalogi związane z jądrem systemu NetBSD oraz odpowiednie moduły Katalog

Moduł

ddb isofs/cd9660

Debugger jądra. System plików ISO CD.

msdosfs

System plików MS-DOS.

netatalk

Obsługa sieci standardu AppleTalk. Obsługa sieci standardu CCITT.

netccitt netinet nfs

Obsługa sieci internetowej. Sieciowy system plików.

vm

Obsługa pamięci wirtualnej.

ufs/mfs

System plików pamięci fizycznej.

ufs/lfs

System plików o strukturze dziennikowej

ufs/ffs

„Szybki” system plików.

A

użycie wspólnego przedrostka nazwy pliku. Przykładowo, pliki w katalogu netinet używają przedrostków if_, in_, ip_, tcp_ oraz udp_ w celu określenia odpowiedniej części stosu protokołu sieciowego, z którym są związane. Ponadto, mimo że moduły zwykłe są przechowywane w pojedynczym katalogu, nie jest zachowana odwrotna zasada — katalogi są często używane w celu grupowania wielu powiązanych ze sobą modułów, które niekoniecznie muszą stanowić większą całość.

9.3.2. Przestrzenie nazw Istotne pojęcie modułu stanowi podstawę mechanizmu ubywania informacji (ang. infor­ mation hiding), który określa, że wszystkie informacje związane z modułem powinny p~| być prywatne, chyba że jawnie określi się je jako publiczne. W modułach języka C implementowanych jako pojedyncze pliki, identyfikatory globalne są deklarowane z użyciem słowa kluczowego s ta tic w celu ograniczenia ich widoczności do pojedyn­ czej jednostki kompilacji (pliku)73. static int zlast - -1: static void islogin(void): static void reexecutetstruct coimand *).

Jednakże technika ta nie zapobiega „przeciekaniu” identyfikatorów używanych w pli­ kach nagłówkowych do plików, które zawierają deklarację ich dołączenia (include). Przykładowo, w językach C i C++ makrodefinicja preprocesora typedef znajdująca się w pliku nagłówkowym może spowodować kolizję, kiedy inny plik definiuje globalną funkcję lub zmienną o tej samej nazwie. Choć niektóre przypadki można rozwiązać poprzez zmianę nazwy powodującego problemy identyfikatora w opracowywanym programie, to w innych, kiedy dwa różne istniejące moduły są ze sobą w konflikcie, znalezienie rozwiązania może być trudne, gdyż mogą one nie znajdować się pod kontrolą programisty. Weźmy pod uwagę poniższy (spreparowany) przykład kompilacji kodu. 73 netbsdsrc/bin/csh/func.c: 64 - 66.

280

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source łinclude “1ibz/zuti1.h" ♦include "Hbc/regex/utUs h"

Dwa zamieszczane pliki nagłówkowe 74,75 definiują identyfikator uch, co powoduje wygenerowanie następującego błędu: libc/regex/uti1s.h:46: redefinition of 'uch'

11bz/zut11.h :36: 'uch' previously declared here

m

Ten problem zanieczyszczenia przestrzeni nazw (ang. namespace pollution) można w języku C rozwiązać na wiele doraźnych sposobów. Inne języki, takie jak Ada, C++, Eiffel, Java. Perl oraz Modula oferują określone konstrukcje pozwalające na jego pokonanie. Często stosowanym rozwiązaniem, nie wymagającym użycia żadnych dodatko­ wych mechanizmów oferowanych przez dany język, jest opatrywanie identyfikatorów określonymi unikatowymi przedrostkami. W poniższym przykładzie wszystkie iden­ tyfikatory typu, funkcji oraz makrodefinicji w pliku nagłówkowym rnd.h 6 opatrzono przedrostkiem rnd. void rnd_attach_source(rndsource_element_t *. char *. u_lnt32_t); void m d detach source(mdsource_element t *);

C...J #def1ne RND MAXSTATCOUNT

10

/* 10 sources at once max */

[...] typedef struct { u_int32_t start; u_1nt32_t count: rndsource_t source[RND_MAXSTATCOUNT]; } rndstat_t;

HI W rzeczywistości metoda przedrostków służąca do izolacji identyfikatorów jest oficjal­ nie sankcjonowana przez standard ANSI C poprzez zarezerwowanie wszystkich identy­ fikatorów rozpoczynających się od znaku podkreślenia (_) do użycia przez implementa­ cję języka. Czytając nagłówek biblioteki, można zauważyć, że wszystkie identyfikatory rozpoczynają się od znaku podkreślenia, co pozwala na ich odseparowanie od innych identyfikatorów, jakie może zdefiniować użytkownik77. struct _ s b u f {

unsigned char *_base; 1nt size:

): Chociaż w powyższym przykładzie znaczniki definiowanej struktury base oraz sue należą — zgodnie z ANSI C — do odrębnej przestrzeni nazw (znaczników struktury _sbuf), wciąż należy je opatrzyć znakiem podkreślenia, gdyż mogą znaleźć się w kon­ flikcie z makrodefmicjami. W celu uniknięcia opisywanych problemów, we współczesnych programach pisanych w języku C++- często używa się mechanizmu przestrzeni nazw (ang. namespace). Lo­ giczna grupa jednostek programu tworzących moduł jest definiowana lub deklarowana 74 netbsdsrc/Iib/libz/zutil.h. 75 netbsdsrc/lib/Ubc/regex/utils.h. 76 netbsdsrc/sys/sys/md.h: 134- 178. 77 netbsdsrc/include/stdio.h: 82-85.

281

Rozdział 9. ♦ Architektura

w bloku namespace (listing 9.10:2)78' 79. Takie identyfikatory mogą być później używa­ ne w innym zasięgu albo poprzez jawne poprzedzenie ich jako przedrostkiem nazwą (zadeklarowanego) identyfikatora w odpowiedniej przestrzeni nazw (listing 9.10:1), albo poprzez zaimportowanie wszystkich identyfikatorów zadeklarowanych w ramach przestrzeni nazw za pomocą dyrektywy using (listing 9.10:3). Druga metoda umożli­ wia używanie identyfikatorów zaimportowanej przestrzeni nazw bez opatrywania ich jakimkolwiek przedrostkiem (listing 9.10:4). Listing 9.10. Definicja przestrzeni nazw ijej użycie wjęzyku C++ namespace OpenCL { class Sklpjack

• — [2] Zakres przestrzeni nazw (skipjack.h)

pubUc BlOCkCiphen#

1

Identyfikator? w-przestrzcm rtan% OpenCL

{

pubUc: static std. stiingLnameO { return "Skipjack": }

[ .. . ] ):

------------------------------------------------------ / I ] Identyfikator dostępu w przestrzeni nazw Std

y • ---------------- Uiycie przestrzeni nazw (hlock.cpp)

1*1n r ) Ude *--------------------------------------------------- Dołączenie nagłówka [...3 fc in g namespace OpenCL.:«---------------------------------------------------------- ¡3] Uzyskanie dostępu t...] do pełnej przestrzeni nazw OpenCL

Fllter* 1ookup_block(const std::string& algname. const 8lockC1pherkeyX key) (

[...] else 1f(algname ” 'Sk 1pjack" ) r ----------------------------------- ¡4] Bezpośredni dmtęjj do identyfikatora return new ECB_F1)ter(key): z przestrzeni nazw OpenCL

Język Java oferuje podobne rozwiązanie problemu zanieczyszczenia przestrzeni nazw dzięki słowu kluczowemu package. Jak pokazano na listingu 9.1180, Sl, słowo to jest używane do określenia, że występujący dalej kod definiuje klasy i interfejsy stanowiące część nazwanego pakietu i może uzyskiwać dostęp do wszystkich typów zawartych w pakiecie. Analogicznie instrukcja import jest używana w celu udostępnienia klas danego pakietu poprzez użycie jego skróconej nazwy w kontekście, w jakim występuje. Ze względu na fakt, że pełne nazwy klas są zawsze dostępne w języku Java, społecz­ ność jego użytkowników powszechnie używa konwencji, według której nazwy wszyst­ kich pakietów są poprzedzane przedrostkiem bazującym na nazwie domeny interneto­ wej lub firmy — twórcy pakietu82' 83, 85.

78 OpenCUinclude/skipjack.h. 11 - 34.

19 OpenCL/checks/hlock.cpp: 36 - 169. 80

jl4/catalina/src/share/org/apache/caialina/core/DefaultContext.java: 65 - 1241.

81

jl4/catalinahrc/share/org/apache/catalina/Host.java. ~ a rg o u m l/org/argotim l/cogniIive/crilics/C ritic.java\ 29.

83 84 85

hsqldb/src/org/ksqldh/W ebServer.java'.

36.

c o coon/src/java/org/apache/cocoon/com ponents/sax/X M L B yteStream C om piler.java: 8.

argouml/org/argouml/ui/ActionGo ToEdit.java: 33.

282

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source package package package package

org.argouml.cognitive.critics: org.hsqldb: org.apache.cocoon.components.sax; ru.novosoft.uml.foundation.core.*:

Listing 9.11. Definicja pakietu ijego użycie wjęzyku Java • — Definicja elementu pakietu iDefaultContext.javaj Pakiet, w którym będą się znajdować definicje

package org.apache.cata 11 na.core;•

im port ja v a .U tll .HaShMap; • ------ Importy elementów pakietów import Java.util.Iterator: import Javax.naming.di rectory.DirContext:

[...] public class OefaultContext^ ------------------ Element należący do pakietu

private HashMap ejbs - new HashMapO;

[...] DirContext dirContext - null:

[...] )

r — Użycie elementu pakietu íHOSt.javaj import org.apache catalina, core Oefaul tContext: • ----------------------------------Import elementu pakietu

[...] public Interface Host extends Container { [... ]

£ ----------------------------------Użycie zaimportowanego elementu

public void addDefaultContext(DefaultContext defaultContextl;

[...] ) Analogiczny mechanizm w języku Ada jest bardzo podobny i bazuje na użyciu słów kluczowych package oraz use. Jedyna różnica polega na tym, że pakiety języka Ada muszą być jawnie importowane przy użyciu słowa kluczowego wi th. Podobnie, język Modula-3 używa deklaracji INTERFACE oraz MODULE w celu definiowania przestrzeni nazw dla interfejsów i implementacji oraz instrukcji FROM ... IMPORT i IMPORT ... AS w celu wprowadzenia nazw do kontekstu lokalnego. Wreszcie, język Perl również ofe­ ruje mechanizm przestrzeni nazw poprzez użycie słów kluczowych package oraz use. Jednakże programiści piszący w tym języku mogą określać sposób uwidaczniania identy­ fikatorów. W większości przypadków w celu określenia listy eksportowanych identy­ fikatorów używany jest moduł Exporter86. package CPAN:

[...] use use use use

Config O; Cwd O: DirHandle: Exporter t):

[...] 0EXPORT - qw( autobundle bundle expand force get cvs_import install make readme recompile shell test clean

):

86perl/lib/CPAN.pm'. 2 - 66.

Rozdział 9. ♦ Architektura

283

9.3.3. Obiekty Obiekt (ang. object) to struktura czasu wykonania zapewniająca dostęp do danych, które zawiera. W systemach czysto obiektowych obliczenia są wykonywane przez obiekty przesyłające i odbierające komunikaty (ang. messages) lub wywołujące metody (ang. methods) innych obiektów. Abstrakcją zachowania podobnych obiektów jest klasa (ang. class), natomiast związane ze sobą klasy są często organizowane w formie hierarchii dziedziczenia (ang. inheritance hierarchy). W językach obiektowych definicję klasy obiektu można rozpoznać po użyciu odpo­ wiedniego słowa kluczowego: class w C++, C#, Eiffel oraz Java, object w Modula-3, package w Ada-95 oraz bless w Perl. Ponadto języki obiektowe powiązane z zintegro­ wanymi środowiskami programowania, takie jak Smalltalk i Visual Basic, obsługują definiowanie i badanie obiektów poprzez wbudowany mechanizm przeglądarki klas (ang. class browser). Wreszcie, nawet w przypadku języków, które nie oferują bezpo­ średniego wsparcia w zakresie tworzenia obiektów, istnieje możliwość ich definiowa­ nia poprzez grupowanie w ramach struktur elementów danych oraz funkcji używanych w celu ich przetwarzania.

Pola i m etody Na listingu 9.12 87 przedstawiono reprezentatywny przykład definicji klasy w języku C++, zaś na listingu 9.1388 — w języku Java. Każda klasa jest definiowana za pomocą

nazwy, używanej później do deklarowania, definiowania i tworzenia obiektów tej klasy. Klasy należące do hierarchii dziedziczenia deklarują również swoją klasę lub klasy nad­ rzędne. Język Java pozwala, aby klasa dziedziczyła tylko po jednej klasie, ale jej defini­ cja może zawierać słowo kluczowe implements, które określa, że implementuje ona funkcjonalność jednej lub większej liczby klas. Klasa zwykle zawiera pewną liczbę zmiennych składowych (ang. member variables), inaczej pól (ang. fields) lub atiybutów (ang. attributes), które przechowują informacje obiektu oraz pewną Wczbą funkcji składowych (ang. member functions), inaczej metod (ang. methods), które wykonują działania na polach obiektu. Każda funkcja składowa jest wywoływana w połączeniu z odpowiednim obiektem89: hash_i nput.f1nal(input_hash):

a stąd może bezpośrednio uzyskiwać dostęp do pól określonego obiektu oraz wywoływać inne metody związane z tym obiektem. Przykładowo, poniższa implementacja metody uzyskuje dostęp do pola crc obiektu (zdefiniowanego na listingu 9.12) i wywołuje jego metodę clearO. void CRC32::firial (byte outputfHASHLENGTH])

(

crc A- OxFFFFFFFF: for(u32b1t j - 0: J !- HASHLENGTH: j++) outputfj] - get_byte(j. crc); clearO:

1 87 OpenCL/include/crc22.h\ 16-31. 88

cocoon/src/jcrva/org/apache/cocoon/util/PostlnputStreamjava: 20 - 255.

89 OpenCL/checks/block.cpp: 121.

284

Czytanie kodu. Punkt widzenia twórców oprogramowania open-sourca

L isting 9 .1 2 . Deklaracja klasy języka C + + dla algorytmu obliczania sumy CRC class CRC32 : • -------------------------------------- Nazwa klasy public HashFunCtlon • ----------------------------- Klasa nadrzędna

i

public: • ----------------------------------------- Początek deklaracji składowych publicznych Static std:: String named { return "CRC32": } • — Pola publiczne (jedna instancja na klasę) static const u32b1t HASHIENGTH - 4. void final (byte[HASHLENGTH]): • ---- Metody publiczne void cleard throwd { crc - OxFFFFFFFF: } CRC320 : HashFunct1on(nameO. HASHLENGTH) { cleard; } • — Konstruktor -CRC320 { Cleard: } • ------------------------ Destruktor private: • --------------------------------------- Początek deklaracji składanych prywatnych Static const u32bit TABLE[256]; • --------------- Jedna instancja na klasę void update_hash(const byte[], u32blt): • ------- Metoda prywatna (zaimplementowana oddzielnie) u32b1t crc: • --------------------------------- Pole prywatne (jedna instancja na obiekt)

L isting 9 .1 3 . Deklaracja klasy języka Java strumienia wejściowego operacji post protokołu HTTP public class PostlnputStreara • ------------------------- Nazwaklasy extends InputStream { • -------------------------------------------------- Klasa nadrzędna

[...] public Static final string CLASS “ • ------------ Pole publiczne (jedna instancja na klasę) Post 1nputSt ream .class. getName C); private InputStream m_inputstream *nuli: • ---------- Pola prywatne (jedna instancja na klasę) private 1nt m_contentLen - 0:

[...] public PostlnputStreamd { • ------------------------Konstruktor super (): • ---- ---------------------------------- Wywołanie metody klasy nadrzędnej

) [...] protected void inittfinal InputStream Input. • — Metoda chroniona final 1nt len) throws IIlegalArgimentException {

[...] t h is m inputS treani « in p u t : # ~

thls.mcontentLen - l e n : H

■- ~

Dostęp do pól przy użyciu

słowa kluczowego this

) [■■■] public InputStream getlnputStreamd { • --------------Metoda publiczna return i m_1 nputStreamdH------------------------ Bezpośredni dostęp do pola

) [...]

Zarówno język C++, jak i Java umożliwia używanie słowa kluczowego th is w celu bezpośredniego odwoływania się do obiektu metody, co ma na celu zwiększenie czy­ telności oraz rozwiązywanie konfliktów nazw.

Konstruktory i destruktory Ze względu na fakt, że obiekty mogą powstawać i znikać na wiele różnych sposobów (jako zmienne globalne lub lokalne, jako dynamicznie tworzone elementy struktur da­ nych lub jako wielkości tymczasowe), istnieje możliwość zdefiniowana specjalnej funkcji konstruktora (ang. constructor) wywoływanej zawsze, gdy obiekt jest tworzony oraz analogicznej funkcji destruktora (ang. destructor), inaczej metody wykończeniowej

Rozdział 9. ♦ Architektura

285

(ang.finalizer), wywoływanej tuż przed zniszczeniem obiektu. Konstruktory definiuje się w językach C++ i Java jako metody o takich samych nazwach jak nazwa odpowied­ niej klasy. Destruktory w języku C++ również zawierają nazwę klasy, ale poprzedza ją znak tyldy (-). Analogiczna metoda w języku Java nosi po prostu nazwę finalize. Konstruktory obiektów są często używane w celu przydzielenia zasobów związanych z obiektem oraz zainicjalizowania jego stanu. Z kolei destruktory są zwykle wykorzy­ stywane do zwalniania zasobów zajętych przez obiekt w czasie jego istnienia. W poniż­ szym przykładzie w momencie tworzenia obiektu zostaje z nim powiązany obiekt nasłu­ chiwania, natomiast w momencie niszczenia obiektu powiązanie to zostaje usunięte90. public abstract class FlgEdgeModelElement [...] { [.

]

public FlgEdgeModelElementO { _name - new F1gText(10. 30. 90. 20); _name.setFont(LABEL_FONT);

t...] ArgoEventPump.addLi stener(ArgoEvent.ANY_NOTATION_EVENT. thls);

} [...] public void finalized { ArgoEventPump.removellstenertArgoEvent.ANY N0TATI0N_EVENT. th1s):

} [ ..] ) W języku C++ pamięć przydzielona dla obiektu za pomocą operatora new musi zostać jawnie zwolniona przy użyciu operatora delete (w języku Java owo zadanie czyszcze­ nia pamięci jest wykonywane automatycznie). Stąd destruktory języka C++ są również używane w celu zwalniania pamięci przydzielonej w okresie istnienia obiektu ' '. isockunlx::1sockunix (const sockunlxbuf& sb) : los (new sockunlxbuf (sb))

{} Isockunlx::-1sockun1x ()

{ delete 1os::rdbuf O;

ł

A W programach języka C++ brak symetrii między pamięcią przydzielaną za pomocą operatora new w konstruktorach obiektów a pamięcią zwalnianą za pomocą operatora d e le te w destruktorach obiektów może stanowić oznakę występowania wycieków pamięci, które należy skorygować. Ponadto, w języku C++ oferowany przez system mechanizm kopiowania obiektów sprawia, że te same dynamicznie przydzielone blo­ ki pamięci są współużytkowane przez dwa obiekty, co w konsekwencji może prowa­ dzić do podwójnego usuwania. W celu zapobieżenia takiej sytuacji musi zostać użyty konstruktor kopiujący (ang. copy constructor) — konstruktor pobierający jako jedyny argument referencję do obiektu. Zwykle wiąże się to albo ze współużytkowaniem obiektu i śledzeniem licznika odwołań (tzw. kopia płytka), albo z wykonaniem pełnej (głębo­ kiej) kopii zawartości obiektu.

90

argouml/org/argouml/uml/diagram/ui/FigEdgeModelElement.java: 63 - 458.

91 socket/sockunix.C: 75-82.

286

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Składow e obiektów i klas Istnieją przypadki, w których dane lub operacje ich przetwarzania są związane z całą klasą, a nie tylko pojedynczymi obiektami. Takie metody klasy (ang. class methods) lub pola klasy (ang. class fields) są w językach C++ i Java reprezentowane poprzez opatrzenie ich przedrostkiem static. Należy zauważyć, że metody klasy nie mogą uzy­ skiwać dostępu do metod lub pól obiektu, gdyż nie są one wywoływane razem z okrep~| ślonym obiektem. Jednak metody obiektu zawsze mogą uzyskać dostęp do metod i pól klasy. Metody obiektu często wykorzystują pola klasy w celu przechowywania danych, które sterują działaniem wszystkich metod (takich jak tabela przeglądowa lub słownik) lub w celu zachowania informacji o stanie związanym z działaniem klasy (na przykład licznik służący do przypisywania unikatowych identyfikatorów każdemu obiektowi). Poniższy przykład ilustruje taki przypadek 92 n . class Test_Any { public: [...] int reset_parameters (void): [...] private. (...] static slze_t counter:

): int Test Any::resetjwrameters (void)

{

[...] CORBA::ULong index - (counter++ % Test Any::ANY_LAST_TEST ITEM);

r..j

l

W idoczność Definicja klasy, oprócz utworzenia kontenera dla danych obiektu oraz określenia współ­ dzielonych metod służących do przetwarzania tych danych, funkcjonuje również jako mechanizm ukrywania informacji. Składowe (pola i metody) mogą być deklarowane jako prywatne (z użyciem słowa kluczowego private) w celu określenia, że dostęp do nich może się odbywać wyłącznie z poziomu metod ich klasy, jako publiczne (z uży­ ciem słowa kluczowego publ ic) w celu określenia, że dostęp do nich może się odbywać z dowolnego miejsca w programie (poprzez obiekty bądź funkcje globalne dla metod klasy) oraz jako chronione (z użyciem słowa kluczowego protected) w celu ogranicze[~n nia ich widoczności do metod ich klasy oraz jej klas pochodnych. W przypadku dobrze zaprojektowanych klas. wszystkie pola są deklarowane jako prywatne i dostęp do nich HI odbywa się poprzez publiczne metody dostępu (ang. access methods). Metody te. któ­ rych nazwy często składają się z nazwy właściwości oraz odpowiedniego przedrostka lub przyrostka get (pobierz) albo set (ustaw), są używane na tyle powszechnie, że języki takie jak C# lub Visual Basic oferują ich wbudowaną obsługę. Ukrywanie pól za meto­ dami dostępu pozwala na wprowadzenie separacji między interfejsem pewnych funkcji (udostępnianych przez metody dostępu) a odpowiadającą im implementacją. Pozwala

92 ace/TAO/tests/Param_Test/any.h: 27 - 97. 92 ace/TAO/tests/Param_Test/any.cpp: 115 - 119.

Rozdział 9. ♦ Architektura

287

to na proste rozszerzanie funkcjonalności pod względem aktualizowania lub (rzadziej) uzyskiwania dostępu do określonych pól. Poniższy przykład zawiera metody dostępu do pola prywatnego bAutoCommit94. public class jdbcConnection Implements Connection ( private boolean bAutoCommit; [. .] public void setAutoCommit(boolean autoCommlt) throws SQLExceptlon ( bAutoCommit - autoCommlt; exeCUteCSET AUTOCOMMIT '• + (bAutoCommit ? "TRUE" : "FALSE"));

} [...] public boolean getAutoCommitO { 1f (Trace.TRACE) { Trace.traceO;

)

return bAutoCommit;

} [...] 1

Metody dostępu są również często używane w celu ograniczenia rodzaju oferowanego dostępu do pól. Poniższy kod definiuje metodę get. ale nie metodę set, co w efekcie powoduje utworzenie pola o dostępie tylko do odczytu9'. public class jdbcDatabaseMetaData implements DatabaseMetaData ( private JdbcConnection cConnectlon; [...] public Connection getConnectiont) { 1f (Trace.TRACE) { Trace traceO;

1

return cConnectlon:

1

PH Wreszcie, w języku C++ metody prywatne są niekiedy używane w połączeniu z prze­ ciążaniem operatorów w celu całkowitego zablokowania działań pewnych operatorów na określonych obiektach96. private: H - Disallow these operations. ACE_UNIMPLEMENTED_FUNC (void operator- (const ACE_Future_Set &))

Niekiedy funkcje, pojedyncze lub wszystkie metody klasy muszą mieć dostęp do skła­ dowych prywatnych innej klasy. W przypadku języka C++, kiedy widzi się funkcję, metodę lub klasę deklarowaną w ramach innej klasy przy użyciu słowa kluczowego friend, zapewnia to pełny dostęp do wspomnianych składowych prywatnych. W po­ niższym przykładzie funkcja main ma dostęp do tablicy chatters i wszystkich innych składowych prywatnych klasy Chatterg'. class Chatter public OHstBoxltem ( [ .] private; static ChatterRef *chatters[MAX HUM CHATTERS]:

[...] friend int maindnt. char *argv[]).

/ / to destroy chatters[]

): 94hsqldb/src/org/hsqldb/jdbcConnection.java: 76 - 997. 95 hsqldb/src/org/hsqldb/jdbcDalabaseMetaData.java: 6 1 - 2970. 96ace/ace/Future_Set.h\ 81 - 83. 97qtchat/src/0.9.7/core/chatter.h\ 36 - 124.

Czytanie kodu. Punkt widzenia twórców oprogramowania opert-source

288

Napotkawszy deklarację friend, należy się zastanowić nad uzasadnieniem powodu przesłonięcia mechanizmu enkapsulacji klasy.

Polimorfizm Istotną cechą obiektów należących do hierarchii dziedziczenia jest możliwość używania obiektów należących do klasy pochodnej jako obiektów klasy nadrzędnej. W niektórych przypadkach klasa nadrzędna zapewnia tylko podstawową funkcjonalność oraz inter­ fejs, który mają zapewnić klasy podrzędne, i nie są tworzone żadne obiekty tej klasy. Tego rodzaju klasy nadrzędne posiadają metody, które mają być oferowane przez podklasy zadeklarowane jako abstrakcyjne przy użyciu słowa kluczowego abstract w języku Java oraz definicji = 0 w języku C++. Za każdym razem, gdy użyta jest metoda obiektu klasy podstawowej (lub wskaźnika na obiekt albo referencji do obiektu w języku C++), system dynamicznie przekierowuje wywołanie do odpowiedniej metody klasy pochod­ nej. Zatem w czasie uruchomienia wywołanie tej samej metody powoduje faktyczne wywołanie różnych metod w zależności od typu odpowiedniego obiektu. Weźmy pod uwagę przykład z listingu 9 .1498' 99, l00-101, lo:. Wszystkie algorytmy (podklasy klasy Algorithm) muszą obsługiwać metodę elear i wszystkie klasy implementujące funkcję mieszającą (jest to sposób wyrażenia długiej wiadomości pod postacią odpowiedniego podpisu, tzw. skrótu) muszą obsługiwać metodę final. Metody te są deklarowane z uży­ ciem słowa kluczowego vi rtual w klasie nadrzędnej, co wskazuje, że będą one obsłu­ giwane przez klasy pochodne. Na listingu przedstawiono odpowiednie deklaracje oraz jedną implementację funkcji elear dla klas MD2 oraz CRC24 — obie stanowią podklasę klasy HashFunction. Funkcja derive, tworząca klucz kryptograficzny przy użyciu algo­ rytmu OpenPGP S2K, różnicuje swoje działanie zgodnie ze składową hash klasy Open «•PGP_S2K. Może ona wskazywać na obiekt należący do dowolnej podklasy klasy Hash «•Function. Kiedy następuje wywołanie funkcji elear lub final, są one automatycznie przekierowywane do odpowiednich metod klasy. Listing 9.14. Polimorfizm czasu uruchomienia wjęzyku C + + ________________________________ class Algorithm ( public: virtual void c l e a r O t h r o w O - 0: •-

class HashFunction : public Algorithm^ public: virtual void final(byteCl) - 0: • —

[...] ):

’Metoda abstrakcyjna implementowana przez wszystkie klasy pochodne klasy Al go n thm ’Spełnienie wymagań interfejsu klasy Al Q0Pi thfll

-Metoda abstrakcyjna implementowana przez wszystkie klasy pochodne klasy HashFunction •Spełnienie wymagań interfejsu klasy HashFunctlOfl

class MD2 : public HashFunction { public: void final(byteCHASHLENGTH]): • ----void c l e a r O throwO:

98 OpenCUinclude/opencl.h: 1 9 -

>gOpenCUinclude/md2.h:

105.

13-28.

11X1 OpenCL/include/crc24.h:

16-29.

101 OpenCL/include/pgp_s2k.h:

ln" OpenCL/src/pgp_s2k.cpp:

13-22

13-59.

Metod V z implementacjami specyficznymi dla klasy

Rozdział 9. ♦ Architektura

289

[...] }: class CRC24 ; public HashFunction { • ------------------------ Spełnienie wymagań interfejsu HashFunctlon public: void final(byte[HASHLENGTH]); •— Melody: implementacjami specyficznymi dla klasy void clearO throwO { crc - 0xB704CE: }

[. ] private: u32bit crc:

): class OpenPGP S2K : public S2K { private: HashFunction*hash: }:

• ------------------------------------ Wskaźnik na obiekt typu HashFunction. Może wskazywać na obiekty MD2.CRC24 lub inne

SymnetricKey 0penPGP_S2K::derive(const std::stringS pw. u32bit keylen) const

{_[•••]

hash - >C1ea r ( ) . # while(keylen > generated)

Wywołaj odpowiednią funkcję klasy pochodnej

{ [...] hash->final (hashbuf); • -----------------------------------Wywołaj odpowiednią funkcję klasy pochodnej

)

return SymmetricKeytkey. key.sizeO):

ł

Przeciążanie operatorów

A

Język C++ pozwala programiście na przedefiniowywanie znaczenia standardowych operatorów języka poprzez metody przeciążania operatorów (ang. operator overloading methods). Mechanizm ten jest często błędnie używany przez początkujących programi­ stów tworzących kod, który wykorzystuje operatory w nieintuicyjny sposób. W prakty­ ce przeciążanie operatorów jest używane sporadycznie w celu zwiększenia użytecz­ ności określonej klasy i znacznie częściej w celu wyposażenia klasy implementującej wielkość liczbową w pełną funkcjonalność związaną z wbudowanymi typami arytme­ tycznymi. Przeciążone operatory są definiowane za pomocą metody lub funkcji typu friend o nazwie składającej się ze słowa kluczowego operator oraz faktycznego sym­ bolu operatora lub typu danych. Funkcje operatora zaimplementowane jako funkcje typu frien d pobierają wszystkie swoje operandy jako argumenty, natomiast te, które zaimplementowano jako metody, uzyskują dostęp do pierwszego operandu jako odpo­ wiedniego obiektu metody. Najczęstsze powody wybiórczego przeciążania niektórych operatorów są następujące. ♦ Dalsze przeciążenie operatorów przesunięcia ( « oraz » ) w celu umożliwienia obsługi sformatowanych danych wejściowych i wyjściowych dla nowego typu danych103. ostream& operator «

1

char buf [1024]: int cont - 1:

103socket/smtp.C: 156- 165.

(ostreami o. smtp& s)

290

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source while (cont) { cont - s.get_response (buf. 1024): o « buf « endl;

}

return o;

) ♦ Udostępnienie operatora porównania, tak aby funkcje biblioteczne wymagające obiektów możliwych do porównywania mogły działać poprawnie104. ACEJNUNE int ACE_Addr::operator —

{

(const ACE_Addr &sap) const

return (sap.addr_type_ — th1s->addr_type_ && sap.addr_s1ze_ -- th1s->addr_slze_ ):

1 ♦ Sprawienie, że obiekty o charakterze wskaźnikowym będą się zachowywać intuicyjnie poprzez przeciążenie operatorów przypisania i wyłuskiwania'"'. * 5>brlef A smart pointer stored in the in-meroory object database * ACE_00B. The pointee (if any) is deleted when reassigned.

*/ class ACE_Export ACE Dumpable_Ptr

1

public: ACE_Dumpable_Ptr (const ACE_Dumpable *dumper »0): const ACE_Dumpable *operator->() const: void operator- (const ACE_Dumpable *dumper) const: private: /// "Real” pointer to the underlying abstract base class II I pointer that does the real work, const ACE_Dumpable *dumper_:

1: ♦ Udostępnienie dla ciągów znaków operatorów przypisania i złączenia obsługiwanych w językach symbolicznych, takich jak Basic, awk lub Perl106. I I concatenation. const CStdString&

operator+-(const CStdString& str):

Kiedy przeciążanie operatorów jest wykorzystywane do tworzenia nowych typów arytmetycznych (takich jak liczby zespolone, ułamki, duże liczby całkowite, macierze lub liczby zmiennoprzecinkowe o dużej precyzji), celem projektanta jest obsłużenie wszystkich operatorów arytmetycznych. Na listingu 9.15107 przedstawiono deklaracje dla klasy obiektów obsługującej arytmetykę liczb całkowitych o nieograniczonym roz­ miarze, która nigdy nie powoduje powstawania przepełnienia. Należy zwrócić uwagę na wiele deklaracji użytych do obsłużenia wszystkich operacji i konwersji. Listing 9.15. Przeciążanie operatorów wjęzyku C++_____________________________________ class Integer

(

public: [...]

104 ace/ace/Addr.i: 19-24. 105 ace/ace/Dump.h: 90-104. 106 demogl/Include/Misc/StdString.h: 363 - 364. 107purenum/integer.h: 7 9 - 199.

291

Rozdział 9. ♦ Architektura // conversion operators Inline operator boolO const: inline operator signed into const: Inline operator unsigned into const: inline operator floatO const: inline operator doubleO const: Inline operator long doubleO const: // unary math operators (members) inline Integer 4operator++(); inline Integer operator++(lnt): Inline Integer 4operator--(); inline Integer operator--(1nt):

// // // //

-Konwersja miętfcy typami

prefix • postfix prefix postfix

—Składowe operatorów jcdnoargumcniowych Ioperand}1dostępne za pomocą konstrukcji

this-») // binary math Inline Integer inline Integer Inline Integer Inline Integer inline Integer inline Integer Inline Integer Inline Integer

operators (members) 4operator-(constInteger 4): •Soperator-(const atom 4): ¿operator-(const satom 4): 4operator+-(const Integer 4): [• 4operator--(const Integer 4): [, 4operator*-(const Integer 4): [. 4operator/-(const Integer 4); [. 4operator*-(const Integer 4): [

-Przypisania różnych typów

~Przykład odmian przypisań

II friends; unary math operators (global functions) friend friend friend friend

Integer operator-;const Integer 4): • ------------------bool operator!(const Integer 4); Integer operator-(const Integer 4); Integer operator*(const Integer 4);

-F unkcje friend operatorów jednoargumentowych (operandje st argumentem funkcji)

II friends: binary math operators (global functions) friend friend friend friend friend friend friend friend friend friend friend friend friend friend friend friend friend

Integer operator+iconst Integer 4. const Integer 4). • ----Integer operator+iconst Integer 4. const atom 4): Integer operator+(const atom 4. const Integer 4): Integer operator+(const Integer 4. const satom 4); Integer operator+(const satom 4. const Integer 4): Integer operator-(const Integer 4. const Integer 4); [...] Integer operator*(const Integer 4. const Integer 4): [...] Integer operator/(const Integer 4. const Integer 4): [...] Integeroperator*(const Integer 4. const Integer 4): [.. ] bool operator— (const Integer 4. const Integer 4); [...] bool operator!-(const Integer 4. const Integer 4): [...] boo) operator>(const Integer 4. const Integer 4): [...] bool operator>-(const Integer 4. const Integer 4); [. bool operator

Number Guess

M u n i. ssize-4> iz e - x ^ ______ l_value)) { • ------ Przetworzenie tokena case LC_RULE:

[...] case LC_Z0NE:

[...]

1St) nethsdsrc/iLsr.sbin/dhcp/server/confpars.c. 167- 192. 157 netbsdsrc/usr.sbin/named/named/db_load.c\ 164 - 277. 158 netbsdsrc/lib/libc/time/zic.c: 773 - 829. 159 hsqldb/src/org/hsqldb/Parser.java: 503 - 533.

314

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Listing 9.25. Analiza składniowa instrukcji SELECTjęzyka SQL private Select parseSelectO throws SQLException (

[...] String token - tTokenlzer.getStringO: • — Pohrametokcna

[ 1f

(token.equalsCDISTINCT")) {

• ---- Przetworzenielokena

[...] ) else 1f( token.equalsCLIMIT")) (

[...] Na listingu 9.26160 można zobaczyć, w jaki sposób struktura interpretera poleceń sed (edytor strumieniowy) opiera się na pętli, która stale pobiera kody instrukcji i wykonuje fTl działania na treści każdej instrukcji. Przetworzony składniowo program jest wewnętrz­ nie przechowywany jako lista jednokierunkowa, w której każdy element zawiera kod instrukcji oraz związane z nią parametry. Zmienna cp stanowi część stanu interpretera (ang. interpreter state) i zawsze wskazuje na wykonywaną instrukcję. Wiele zmiennych, takich jak appendx, przechowuje stan programu (ang. program state). Większość spoty­ kanych interpreterów posiada podobną architekturę przetwarzania wykorzystującą ma­ szynę stanów, której działanie zależy od bieżącego stanu interpretera, instrukcji programu oraz stanu programu. Listing 9.26. Interpreter poleceń sed top: cp - prog: • ---------------------------redirect: while (cp !- NULL) { • -------------------

-Początek programu ■Czyje st dostępna instrukcja''

[...] switch (cp->code) { • ---------------case 'a': • -------------------------1f (appendx >- appendnum) appends - xrea Hoc (appends, sizeofistruct s_appends) * (appendnum *- 2)): appends[appendx].type - AP_STRING: appends[appendx].s - cp->t: appends[appendx].len - strlen(cp->t) appendx++: break: case 'b': • -----------------cp - cp->u.c: goto redirect: case ‘c ’:

-Wybór akcji zależnej od instrukcji ■Kod instrukcji ■Instrukcje przetwarzające

-Instrukcja rozgałęzienia pobranie nowego wskaźnika kodu i kontynuacja działania

[...] case 'd':

[...] case 'O':

[...] case ’q -: • -------------if (inflag && ipd) OUT(ps) flush_appends(): exlt(O):

160 netbsdsrc/usr.bin/sed/process.c. 105 - 257.

■Instrukcja quit (wyjścia)

Rozdział 9. ♦ Architektura [ . .] • } CP * cp->next: #

315

'

K od przetwarzania dla kolejnych ośmiu kodów instrukcji -Pobranie kolejnej instrukcji

} /* for dli cp */

Inna dość często spotykana klasa architektur dziedzinowych nosi nazwę architektur referencyjnych (ang. reference architectures). W wielu przypadkach architektury refe­ rencyjne określają strukturę notacyjną w obszarze zainteresowania, która nie zawsze jest przestrzegana przez konkretne implementacje. Architektura koncepcyjna Open Systems Interconnection (OSI) stanowi tu adekwatny przykład. Architektura ta określa, że systemy sieciowe mają być dekomponowane do postaci stosu siedmiu warstw sie­ ciowych: warstwy aplikacji, prezentacji, sesji, transportu, sieci, łącza danych oraz fizycz­ nej. Często można spotkać się z argumentacją dotyczącą funkcjonalności określonych warstw161. * DUMB!! Until the mount protocol works on Iso transport, we must * supply both an 1so and an (net address for the host

Jednak rzadko można spotkać implementacje programistyczne o strukturze odpowia­ dającej siedmiu warstwom zalecanym przez model ISO. Kwestie związane z wydajno­ ścią, uwarunkowaniami historycznymi oraz sprzecznymi wymaganiami co do współ­ pracy sprawiają często, że powstają implementacje, w których różne warstwy ISO są obsługiwane przez ten sam moduł oprogramowania lub różne podsystemy współpracują w celu obsłużenia jednej warstwy. Przykładowo, warstwa sesji zwykle nie ma zasto­ sowania w przypadku protokołu TCP/IP, natomiast systemy Unix BSD zwykle dzielą obsługę określania tras między prosty moduł jądra 162 i bardziej wyrafinowane programy użytkownika, takie jak route 163 lub routedX(A. Ćwiczenie 9.22. Znajdź na płycie dołączonej do książki program, który utworzono na podbudowie schematu strukturalnego (w jaki sposób będziesz go szukać?). Oszacuj liczbę wierszy kodu odpowiedzialnego za przetwarzanie i porównaj otrzymany wynik z wielkością potrzebną do spełnienia wymagań schematu. Ćwiczenie 9.23. Użyj generatora kodu w celu w celu utworzenia aplikacji. Zbadaj kod wynikowy i zidentyfikuj części, które stanowią abstrakcję wynikającą z użycia mecha­ nizmów innych niż automatyczne generowanie kodu. Ćwiczenie 9.24. Komunikacyjny schemat strukturalny ACE16’ wykorzystuje wiele róż­ nych wzorców: Acceptor. Active Object, Adapter. Asynchronous Completion Token, Component Configurator, Connector, Decorator, Double Checked Locking, Extension Interface, External Polymorphism, Half-Sync/Half-Async, Interceptor, Iterator, Leader/ Followers, Monitor Object, Object Lifetime Manager, Observer, Proactor, Reactor, Scoped Locking, Service Configurator, Singleton, Strategized Locking, Strategy Bridge, 161 netbsdsrc/sbin/mount_nfs/mountjifs.c\ 577 - 578. 162 netbsdsrc/sys/net/route.c. 163 netbsdsrc/sbin/route. 164 netbsdsrc/sbin/routed. 165 ace/ace.

316

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Thread-per Request, Thread-per Session, Thread-Safe Interface, Thread Pool, Visitor oraz Wrapper Facade. Dla każdego wzorca zlokalizuj odpowiedni kod i odwołując się do dokumentacji wzorców, zidentyfikuj w kodzie wyróżniające cechy każdego z nich. Ćwiczenie 9.25. Pobierz kod źródłowy kompilatora GNU C 166 i określ, w jaki sposób części jego kodu wpasowują się w architekturę typowego kompilatora. Ćwiczenie 9.26. Omów, w jaki sposób części kodu obsługi sieci systemu NetBSD167' 168.169, no 0(ip 0Wja(}ają stosowi sieci standardu ISO.

Dalsza lektura Architektura oprogramowania stanowi temat kilku pozycji [Sha95, SG96, BCK98, BHH99], Podobnie jest w przypadku wzorców projektowych [CS95, GHJV95, BMR’96, SSRB00]. Ogólny zarys architektury klient-serwer można znaleźć w pozycji Sinha’ego [Sin92], natomiast szczegółowe omówienie wielu internetowych aplikacji typu klient-serwer w pozycji Comera i Stevensa [CS96]. Systemy tablicowe zwięźle opisano w po­ zycji Hunta i Thomasa [HT00, s. 155 - 170], Omówiona w rozdziale specyfikacja opro­ gramowania pośredniczącego została opisana w dokumentach poświęconych standardom CORBA [Obj02, Sie99], DCOM [Mic95], Java RM1 [Sun99b] oraz Sun RPC [Sun88a, Sun88b]. System Unix oferuje wiele udogodnień w zakresie obsługi architektur prze­ pływu danych: patrz pozycja Kemighana i Pike’a [KP84] oraz Kemighana [Ker89], w której opisano sposoby ich wykorzystania w zakresie składu dokumentów. Architek­ tura przepływu danych wykorzystująca potoki i filtry została opisana jako wzorzec pro­ jektowy w pracy Meuniera [Meu95], Język UML używany do modelowania architektur obiektowych zdefiniowano w pracy Raumbaugha i in. [RJB99] oraz opisano w pozy­ cji Boocha i in. [BRJ99]. Artykuł Danielsa [Dan02] zwięźle opisuje sposoby użycia różnych diagramów UML w praktyce. Narzędzie ArgoUML przedstawiono w pracy Robbinsa i Redmilesa [RR00]. Pojęcie warstw zostało użyte po raz pierwszy jako za­ sada projektowa w implementacji systemu operacyjnego THE autorstwa Dijkstry i stało się podstawowym narzędziem w rozwoju systemów operacyjnych [Org72], sieci kompu­ terowych [DZ83] oraz warstwy przenośności języka Java [LY97]. Jednak warto również zapoznać się z pracą naukową opisującą różnorodne zagadnienia związane z projekto­ waniem systemów [SRC84], jak również z inną pracą, poświęconą kwestiom architektu­ ry, które mają wpływ na tradycyjnie zaimplementowane w sposób warstwowy proto­ koły sieciowe [CT90], Przecinanie początkowo opisał Weiser [Wei82], W późniejszych pracach można przeczytać o tym, jak przecinanie pomaga w zrozumieniu i dokonywa­ niu inżynierii wstecznej kodu [BE93, JR94, WHGT99], Pojęcia sprzężenia i zwartości zdefiniowali Constantine i Yourdon [CY79]. Związki istniejące między przecięciami 166 http://www.gnu.org/software/gcc. 167 netbsdsrc/sys/net. 168 netbsdsrc/sys/netinet. 169 netbsdsrc/sys/netiso. 170 netbsdsrc/sys/netnatm.

Rozdział 9. ♦ Architektura

317

a zwartością analizowali Ott i Thuss [OT89]. Prawo Demeter opisano w pracy Lieberherra i Hollanda [LH89], jak również w pracy Hunta i Thomasa [HTOO, s. 140 - 142], Więcej informacji na temat zarządzania procesami w systemie BSD Unix można znaleźć w pracy Lefflera i in. [LMKQ88, s. 69 - 165]. Często stosowane architektury oparte na zdarzeniach opisano w formie wzorców w pracy Rana [Ran95], Demultipleksacja zda­ rzeń stanowi temat pracy Schmidta [Sch95], Podejścia do zagadnienia pakietowania elementów omówione w podrozdziale 9.3 bazują na modelu właściwości strukturalnych pochodzącym z pracy Shawa i Garlana [SG95], Więcej informacji na temat modulamości można znaleźć w pracy Meyera [MeyOO] oraz wciąż aktualnej pozycji Pamasa [Par72]. Przydatną pozycją poświęconą językom obiek­ towym jest praca Salusa [Sal98], W przypadku konkretnych języków, sztandarowymi pozycjami są prace: Stroustrupa [Str97] dla języka C++, Microsoft [MicOl] dla C#, Arnolda i Goslinga dla języka Java, Conwaya [ConOO] dla obiektowego Perlą oraz Goldberga [Gol80] dla języka Smalltalk. Więcej szczegółów dotyczących sposobów wewnętrznej implementacji obiektów można znaleźć w pozycji Ellis i Stroustrupa [ES90], Metody abstrakcyjne omawia pozycja Plaugera [Pla93, s. 177 - 209], zaś programo­ wanie ogólne w ramach języka C++ dwa inne źródła [Aus98, AleOl], Kilka kolejnych pozycji [BW98, Szy98, Wil99, SR00] omawia bardziej szczegółowo tworzenie opro­ gramowania w oparciu o komponenty. Raporty, opis aplikacji oraz problemów napoty­ kanych w przypadku dziedzinowych schematów strukturalnych można znaleźć w pra­ cach Fayada i in. [FJ99, FJS99a, FJS99b]. Metoda tworzenia dokumentacji schematów strukturalnych przy użyciu wzorców została zaprezentowana w pracy Johnsona [Joh92]. Wiele książek poświęcono wzorcom i językom wzorcowym [GHJV95, BMR+96, RisOO, Tic98, Vli98]. Można również znaleźć wiele interesujących artykułów w materiałach z konferencji International Conference on Pattern Languages o f Programming oraz European Conference on Pattern Languages o f Programming [CS95, CKV96, MRB97, HFR99]. System Adaptive Communication Environment (ACE) zamieszczony na pły­ cie CD-ROM dołączonej do książki171 opisano szczegółowo w pozycji Schmidta i in. [SSRB00]. Jeżeli zagadnienie to zainteresuje Czytelnika, ciekawa może również okazać się lektura pozycji Alexandra na temat wzorców projektowych [Ale64, A1S’77]. Klasyczną pozycją z zakresu architektury procesorów językowych jest książka Aho i in. [ASU85], Inne obszary, w których ewoluowały architektury dziedzinowe, to systemy operacyjne [Tan97], sieci komputerowe [ComOO, CS98] oraz systemy typu klient-serwer [DCS98].

171 ace/ace.

Rozdział 10.

Narzędzia pomocne w czytaniu kodu Czytając kod, zwykle ma się dostęp do jego wersji elektronicznej. Oznacza to, że ist­ nieje możliwość jego przetwarzania za pomocą określonych narzędzi w celu zwięk­ szenia wydajności jego czytania i rozumienia. Poniżej wymieniono typowe zadania wykonywane na dostępnym kodzie. ♦ Identyfikowanie deklaracji określonego elementu lub określanie typu funkcji, zmiennej, metody, szablonu lub interfejsu. ♦ Lokalizowanie definicji określonego elementu, na przykład znajdowanie treści funkcji lub klasy. ♦ Przeglądanie miejsc, w których jest używany określony element. ♦ Utworzenie listy odchyleń od standardów kodowania. ♦ Znajdowanie struktur kodu, które mogą pomóc w zrozumieniu określonego fragmentu programu. ♦ Znajdowanie komentarzy wyjaśniających określoną funkcję. ♦ Sprawdzenie występowania często spotykanych błędów. ♦ Przeglądanie struktury kodu. ♦ Zrozumienie sposobu interakcji programu ze środowiskiem. W niniejszym rozdziale zostaną opisane narzędzia, których można używać w celu zauto­ matyzowania wymienionych powyżej zadań oraz wykonywania ich w sposób najwydaj­ niejszy z możliwych. Ponadto, narzędzia służące do modelowania często mogą pomóc w dokonaniu inżynierii wstecznej architektury systemu, zaś wiele narzędzi dokumen­ tujących może automatycznie utworzyć dokumentację projektu na podstawie odpowied­ nio sformatowanego kodu źródłowego. Więcej informacji na temat użycia tego rodzaju narzędzi można znaleźć w podrozdziałach 9.1.3 oraz 8.5. Wiele narzędzi i przykładów prezentowanych w niniejszym rozdziale bazuje na środowisku uniksowym i dostępnej

320

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

w nim rodzinie narzędzi. Jeżeli oprogramowanie używane przez Czytelnika nie jest z nim zgodne, powinien zapoznać się z ostatnim podrozdziałem, gdzie znajdzie informa­ cje na temat możliwości zdobycia kompatybilnych narzędzi. Prezentowane oprogra­ mowanie wykazuje stopniowe coraz bliższe podobieństwo między kodem źródłowym a jego wykonaniem. Najpierw zostaną przedstawione narzędzia działające na kodzie źródłowym na poziomie leksykalnym (oznacza to, że przetwarzają one znaki bez analizy składniowej struktury programu), później przejdziemy do narzędzi bazujących na anali­ zie składniowej i kompilacji kodu, a na koniec omówimy narzędzia związane z wyko­ nywaniem kodu.

10.1. Wyrażenia regularne

A

Narzędzia działające na kodzie źródłowym na poziomie leksykalnym oferują ogromne, często niedoceniane możliwości. Mogą być używane w przypadku dowolnego języka oprogramowania i platformy, działają bez potrzeby wstępnego przetwarzania lub kom­ pilowania plików, oferują możliwość wykonywania wielu zadań, są szybkie i obsługują dowolnie duże ilości tekstu kodu. Używając takich narzędzi, można efektywnie wyszukiwać określone wzorce w dużym pliku źródłowym lub w wielu plikach. Ze swej natury narzędzia te są nieprecyzyjne, jedn ich użycie może pozwolić zaoszczędzić wiele czasu i daje wyniki, które często nie są możliwe do uzyskania w przypadku samodziel­ nego przeglądania kodu. Wydajność i elastyczność wielu narzędzi leksykalnych wynika z użycia wyrażeń regu­ larnych (ang. regular expressions). Wyrażenie regularne można postrzegać jako przepis na dopasowanie ciągu znaków. Składa się ono z sekwencji znaków. Większość znaków odpowiada po prostu samym sobie, ale istnieją również znaki specjalne, tzw. metaznaki (ang. meia-characters), które mają szczególne znaczenie. Wyrażenia regularne tworzy się, używając połączenia znaków regularnych oraz metaznaków w celu określenia prze­ pisu na dopasowanie konkretnych elementów kodu, których się szuka. W tabeli 10.1 przedstawiono elementy najczęściej używane przy konstruowaniu wyrażeń regularnych.

Tabela 10.1. Często stosowane elementy konstrukcyjne wyrażeń regularnych Znak

Dopasowanie

Labe)

Dowolny znak Dowolny ze znaków a, b lub c (klasa znaków)

Lsabel

Wszystkie znaki oprócz a, b i c

a* \m -

Zero lub większa liczba wystąpień znaku a

i \


Koniec wyrazu

Metaznak m Początek wiersza

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

321

Większość edytorów oferuje polecenie wyszukiwania ciągów znaków przy użyciu wy­ rażeń regularnych. W tabeli 10.2 zawarto określone polecenia używane przez popularne edytory. Składnia wyrażeń regularnych, przedstawiona w tabeli 10.1, jest identyczna w przypadku większości z nich i jest obsługiwana przez takie narzędzia jak W, Emacs czy Epsilon. Większość edytorów rozszerza składnię wyrażeń regularnych o dodatkowe funkcje, niektóre zaś używają nieco innej składni. Przykładowo, edytor BRIEF używa znaku ? dla dopasowania dowolnego znaku, %— dla dopasowania początku wiersza, - — dla określenia niepasującej klasy znaków oraz + — dla wyrażenia jednego lub większej liczby dopasowań ostatniego wyrażenia regularnego. W celu zapoznania się z pełną składnią wyrażeń regularnych używaną przez dany edytor, należy sięgnąć do jego dokumentacji. Tabela 10.2. Polecenia wyszukiwania za pomocą wyrażeń regularnych w przypadku różnych edytorów Edytor

Polecenie wyszukiwania w przód

Polecenie wyszukiwania wstecz

BRIEF0

Search Forward

Search Backward

Emacs Epsilon vi

C-M-s6 isearch-forward-regexp C-A-sc regex-search 1

C-M-r isearch-backward-regexp C-A-R reverse-regex-scarch ?

Visual Studio

FindRegExpr

FindRegExprPrev

° Po przejściu do trybu wyrażeń regularnych. ‘ Control-Meta-s. cControI-Alt-s.

A

Posiadając edytor obsługujący wyrażenia regularne oraz podstawową wiedzę na temat używanej składni, można go wykorzystać do efektywnego przeglądania obszernych plików źródłowych. W celu uniknięcia przypadkowej zmiany treści pliku, należy upewnić się, że nie posiada się prawa do zapisu do plików, lub skorzystać z edytora w trybie tylko do odczytu (na przykład v1 -R w przypadku edytora w).

Na listingu 10.11 przedstawiono pewne przykłady rodzajów wyszukiwania, jakie wy­ konuje się przy użyciu wyrażeń regularnych. Często można zlokalizować określone elementy programu wykorzystując styl kodowania stosowany przez programistę. Moż­ na na przykład bazować na definicji funkcji (listing 10.1:1). Większość stylów progra­ mowania określa, że definicje funkcji powinny być zapisywane z jej nazwą rozpoczy[~i~l nającą nowy wiersz. Stąd, definicję funkcji można zlokalizować używając wyrażenia regularnego typu *nazwa funkcji. Takie wyrażenie regularne odpowiada wszystkim wierszom, które rozpoczynają się od nazwa funkcji. Odpowiada ono również funkcjom, których nazwa rozpoczyna się od tego samego ciągu znaków co nazwa poszukiwanej funkcji. Można jednak nieco zmodyfikować wyrażenie regularne, dopasowując je do nazwy funkcji jako pełnego słowa: *nazwa funkcji\>. Listing 10.1. Dopasowania do wyrażenia regularnego struct fpn *

[ I] '•fpu sqrt

1 netbsdsrc/sys/arch/sparc/fpu/fpu_sqri.c\ 189-300.

322

Czytanie kodu. Punkt widzenia twórców oprogramowania opert-source

register register register register register

struct fpn Ą. - &fe->fe_fl:I------------- ¡3] \'x u_1nt bit. q. tt; ujnt x0. xl. x2. x3: u_1nt yO. yl. y2. y3;*------------------ [4] [xydj[0 - 3j ujnt dO. dl. d2. d3:

[...] #def1ne DOUBLE_X { \ FPU ADOS(x3. x3. x3); FPU A0DCS(x2. x2. x2); »

Ili

FPU_ADDCS(xl. xl. xl): FPU_ADDC(xO. x0. x0); \

[...] q - bit: x0 — bit:«---------------------------------------------------------------- (5]x0.*bit

[...] tL - bit;

i---------------------------------------- [ 6 ] tl.T = ] -M

W praktyce okazuje się, że określenie prostego wyrażenia regularnego daje bardzo dobre rezultaty w zakresie lokalizowania poszukiwanego tekstu, a bardziej skomplikowane wyrażenia trzeba podawać jedynie w przypadkach, gdy wersja prosta daje zbyt wiele i n fałszywych dopasować. W pewnych sytuacjach okazuje się również, że wystarczy okre­ ślić jedynie fragment poszukiwanego ciągu znaków. W innych, kiedy szuka się zmien­ nej posiadającej krótką nazwę, występującej w wielu kontekstach, należy bezpośrednio określić ograniczniki wyrazu (\) — listing 10.1:2. Niestety, wiele metaznaków wyrażeń regularnych jest również używanych przez języki programowania. Dlatego trzeba pamiętać o opatrywaniu ich znakiem kontrolnym (odwrotnym ukośnikiem — \), kiedy się szuka elementów^ programu, które je zawierają (listing 10.1:3). Klasy znaków wyrażeń regularnych są przydatne w przypadku wyszukiwania wszystkich zmiennych o nazwie pasującej do pewnego wzorca (listing 10.1:4), natomiast klasy znaków z negacją mogą być używane w celu uniknięcia fałszywych dopasować. Przykładowo, wyrażenie z listingu 10.1:6 lokalizuje przypisanie do zmiennej t l (znalezienie miejsc, w których zmienna podlega modyfikacji stanowi często podstawę zrozumienia kodu) bez dopa­ sowania wierszy, w których t l występuje po lewej stronie operatora równości (==). HI Metaznak zero-lub-więcej dopasować (*) jest często używany razem z metaznakiem do­ pasowania do dowolnego znaku (.) w celu określenia dowolnej ilości nieznanego tekstu (. *). W razie określenia takiego wyrażenia, kod dopasowywania nie zwraca wszystkich znaków do końca wiersza, ale próbuje dopasować tylko tyle znaków, aby utworzyć do­ pasowanie do całego wyrażenia regularnego (w tym do części występującej za sekwen­ cją .*). Na listingu 10.1:5 cecha ta została wykorzystana w celu zlokalizowania wszyst­ kich wierszy zawierających zmienną x0, po której występuje zmienna b it. Ćwiczenie 10.1. Zapoznaj się ze składnią wyrażeń regularnych obsługiwaną przez używany edytor. Poeksperymentuj z oferowanymi dodatkowymi funkcjami. Spróbuj wyrazić dodatkowe funkcje używając wyłącznie metaznaków, które opisano powyżej. Omów zależności istniejące między bogactwem składni wyrażeń regularnych a kosz­ tem nauczenia się jej. Ćwiczenie 10.2. Zapisz wyrażenie regularne lokalizujące liczby całkowite, zmienno­ przecinkowe oraz określony wyraz w ciągu znaków. W jakich sytuacjach można by użyć tych wyrażeń? Ćwiczenie 10.3. Zapisz wyrażenie regularne lokalizujące odchylenia od stylu kodowania w czytanym kodzie źródłowym.

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

323

10.2. Edytor jako przeglądarka kodu Niektóre edytory (np. Emacs lub W) wykorzystują wyszukiwanie z użyciem wyrażeń regularnych wraz z plikiem indeksowym, co pozwala na wydajne lokalizowanie różnych definicji w kodzie źródłowym. Najpierw tworzy się plik indeksowy używając narzędzia clags względem wszystkich plików źródłowych, które nas interesują. Plik indeksowy 0 nazwie tags zawiera posortowany zbiór definicji znalezionych w plikach źródłowych. W przypadku definicji języka C rozpoznawane są funkcje, klauzule #deftne oraz. opcjo­ nalnie, klauzule typedef, struktury, unie oraz wyliczenia. Każdy wiersz pliku tags zawie­ ra nazwę rozpoznanego elementu, nazwę pliku, w którym go znaleziono, oraz wzorzec wyrażenia regularnego służący do zlokalizowania tego elementu w pliku. Poniższe wiersze stanowią fragment pliku tags dla kodu źródłowego programu secf" '. NEEDSP process.c /-#define NEEOSPireqlen) compile_addr compile. c /Acompile_addr(p. a)S/ e_args defs.h /‘entim e_args {$/ s_addr defs.h /-'struct s_addr {$/

US/

Użycie wyrażeń regularnych zamiast numerów wierszy pozwala na zachowanie aktu­ alności pliku tags nawet po dodaniu lub usunięciu części z nich z pliku źródłowego. Przeglądając kod źródłowy, można użyć specjalnego polecenia (A] w przypadku edyto­ ra vi oraz M- w przypadku Emacs) w celu zbadania nazwy elementu w miejscu kurso­ ra i przejścia do punktu, w którym występuje jego definicja. Zatem używając funkcji znaczników, można szybko przeglądać definicje nowo napotkanych elementów kodu. Wykorzystując zwykły format tekstowy pliku tags, można z łatwością napisać proste narzędzie tworzące specyficzne pliki znaczników dla innych języków programowania lub specyficznych aplikacji. Program w języku Perl przedstawiony na listingu 10.2 tworzy plik tags na podstawie kodu źródłowego języka Microsoft Visual Basic, co pozwala na jego przeglądanie przy użyciu dowolnego edytora obsługującego znaczniki. Precyzyjne formatowanie plików źródłowych języka Visual Basic upraszcza działanie prezentowanego programu. Jego pętla główna przechodzi przez wszystkie pliki znajdujące się w katalogu bieżącym 1 dopasowuje wiersze (z użyciem rozbudowanej składni wyrażeń regularnych języka Perl), które wyglądają na definicje struktur sterujących, podprogramów lub funkcji. [Tl Tablica 1ines zawiera listę wszystkich wygenerowanych wzorców wyszukiwania, tak więc kiedy wszystkie dane wejściowe zostaną odczytane, można je zapisać w postaci posortowanej do pliku tags. Chociaż program można by ulepszyć na wiele sposobów, jest on wystarczający do wykonania większości zadań. Listing 10.2. Generowanie znaczników na podstawie plików źródłowych języka Visual Basic_________ @ARGV • (< *. f rm>. . < *. C tl > ): # wh 11 e (o ) { Chop:

i f ( / B e g in \ s - r

Pliki źródłowe języka Visual Basic #

-Przejście prze: wszystkie wiersze wszystkich plików Czy

to struktura sterująca?

2 netbsdsrc/usr.bin/sed. 3 O ile nie zostanie określone inaczej, wszystkie przykłady w niniejszym rozdziale dotyczą tego samego

zbioru plików źródłowych.

324

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source push(@lmes. ■Jl\tiARGVU?t_?\n")»} elsif (/(Sub|Function)\s+(\w+)*H --push(?l1nes. “S2UiARGV\t'H ?\nfc—

open(T, '»tags') || die “tags: S!\nM:•— print T sort Plines:

-Dodanie wiersza znacznika do tablicy tags -C zy to podprogram lub funkcja? -Dodanie wiersza zawierającego nazwę elementu, pliku i wzorzec

” Wygenerowanie posortowanego pliku tags na podstawie tablicy' tags

Wiele narzędzi korzysta z oryginalnej idei działania programu ctags. ♦ Narzędzia indeksujące idutils4 tworzą bazę danych identyfikatorów programu oraz ich występowania w zbiorze plików. Następnie odrębne narzędzia działające w wierszu poleceń mogą być używane w celu wykonywania szeregu różnych działań. Chodzi tu o zapytania względem bazy odnośnie do plików zawierających określony identyfikator, edytowanie zbioru plików, które spełniają warunki zapytania, tworzenie listy tokenów występujących w pliku lub użycie programu grep w celu przetworzenia podzbioru plików zawierających identyfikator i przekazania na wyjście wierszy, w których się pojawia. Narzędzia idutils można rozbudowywać o określone przez użytkownika programy przeglądające. Są one obsługiwane przez wiele edytorów, takich jak Emacs i nowsze wersje vi, na przykład vim. W porównaniu z narzędziem ctags idutils obsługują szerszy zakres „identyfikatorów", w tym literały oraz nazwy plików dołączanych. ♦ Narzędzia indeksujące exuberant ctags5 rozszerzają funkcjonalność programu ctags, oferując wsparcie dla 23 różnych języków programowania. Przeglądając duże pliki źródłowe, można zapoznać się z widokiem kodu „z lotu ptaka”, korzystając z opcji podglądu edytora. Edytor GNU Emacs oferuje polecenie selective display (wyświetlanie selektywne) C-x $—set-selective-display, umożliwiające ukry­ cie wierszy wciętych o liczbę kolumn większą niż określona. Polecenie to pobiera jako argument numeryczny liczbę wciętych kolumn, jakie należy ukryć. Podając odpowied­ nią wartość, można ukryć głęboko wcięte partie kodu źródłowego. Można również skorzystać z trybu edytora Emacs outline mode (tryb podglądu) M-x outl ine-mode oraz odpowiednich definicji wierszy nagłówkowych w celu szybkiego przełączania się mię­ dzy drzewiastym widokiem pliku a treścią odpowiednich wierzchołków. Edytor vim również obsługuje opcję podglądu poprzez polecenie folding (zwijanie), na przykład set foldenable. Zm. Zr. Zo. Zc. Pozwala ona na zwijanie części kodu źródłowego i otrzymanie ogólnego obrazu struktury pliku. Jednym z interesujących sposobów szybkiego zapoznania się z ogólną strukturą pliku źródłowego w systemie Windows jest jego załadowanie do edytora Microsoft Word, a następnie ustawienie powiększenia widoku na wartość bliską 10%. Każda strona kodu będzie wówczas miała mniej więcej rozmiar znaczka pocztowego, dzięki czemu na podstawie kształtu wierszy można uzyskać zaskakująco wiele informacji o strukturze kodu. Przykładowo, na rysunku 10.16 można rozpoznać początkowe instrukcje importu, bloki metod oraz głęboko wcięte fragmenty kodu. 4 http://www.gnu.org/directory/idutils.html. 5 http://ctags.sourceforge.net/. 6 cocoon/src/java/org/apache/cocoon/transformation/LDAPTransformer.java.

325

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

1 3 1 1 D A P T r a n s f o r m o r (P od gl ąd ) • Microsoft W o r d EJk L

Edycja

yfidok

4f i¡ 2 4 6 8

sekcja 1

Wjtaw

Format

tyarzędzia

■ Tabela

Qkno

Pomoę

H

d

l

u

M

I -

101214

1/27

Po*. 2,5 o n

wrs 1

Kol. 1

Angielski (U

QQ

Rysunek 10.1. Podgląd kodu źródłowego w edytorze Microsoft Word Edytora można również używać w celu wykrywania par nawiasów okrągłych, kwadra­ towych i klamrowych. Odpowiednie polecenia znowu różnią się w przypadku różnych edytorów. Emacs oferuje szereg poleceń operujących na listach: C-M-u (backward up- i ist) pozwala przechodzić w przód i w tył struktury listy, C-M-d (down-1 is t) przechodzi w dól i w górę, C-M-n (forward-l i s t ) przechodzi do przodu, zaś C-M-p (backward-l i s t ) do tyłu. Edytor w oferuje polecenie I, które przenosi kursor do pasującego drugiego naw ia­ su. Wreszcie, w środowisku Microsoft Visual Studio można używać poleceń Level Up oraz Level Down w celu przechodzenia na różne poziomy oraz poleceń GoToMatchBrace i GoToMatchBrace-Extend w celu wykrywania lub zaznaczania tekstu, który odpowdada danemu fragmentowi w nawiasach klamrowych. Ćwiczenie 10.4. Zapoznaj się i poeksperymentuj z mechanizmem obsługi znaczników używanego edytora. Czy można rozszerzyć jego funkcjonalność za pomocą dodatko­ wych narzędzi? Ćwiczenie 10.5. W celu zlokalizowania katalogu komunikatów wyjściowych edytora vi, należy skopiować plik bazowy dla języka angielskiego i zastąpić komunikaty cią­ gami znaków we własnym języku. Niestety, aby zidentyfikować charakteru argumentu każdego z komunikatów, należy przeszukać kod źródłowy dla każdego z nich . Utwórz narzędzie wykorzystujące znaczniki w celu zautomatyzowania tego procesu.

7 netbsdsrc/usr.bin/vi/katalog/README: 113- 125.

326

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Ćwiczenie 10.6. Zaproponuj dodatkowe sposoby wykorzystania mechanizmu znacz­ ników. Uwzględnij duże partie kodu, takie jak jądra systemu Unix8. Ćwiczenie 10.7. Czy edytor środowiska programowania, w którym pracujesz, obsługuje podgląd ogólnej struktury pliku źródłowego? Poeksperymentuj z tą funkcją, używając przykładów zawartych na płycie dołączonej do książki.

10.3. Przeszukiwanie kodu za pomocą narzędzia grep Duże projekty zwykle dzielą się na wiele plików, które czasem są zorganizowane w strukturę katalogów. Przeszukiwanie każdego pliku za pomocą edytora nie zawsze jest rozwiązaniem praktycznym. Na szczęście istnieją narzędzia, którą pozwalają zautomatyzować to zadanie. Protoplastą wszystkich narzędzi używanych do przeszu­ kiwania dużych partii kodu jest program grep, którego nazwa wywodzi się od polecenia edytora ecUex. drukującego wszystkie wiersze pasujące do wzorca (g/RE/p — globalnie w ramach pliku znajdź wiersze, które pasują do podanego wyrażenia regularnego i wy­ drukuj je), grep pobiera jako argument wyrażenie regularne, które ma wyszukać, oraz listę plików. Pliki są zwykle specyfikowane przy użyciu wzorca zawierającego wieloznaki. na przykład *.c * .h. Wiele znaków używanych w wyrażeniach regularnych posiada również specjalne znaczenie dla powłoki wiersza poleceń, tak więc najlepiej jest ujmować je w cudzysłowy. Poniższa sekwencja wyświetla nazwę pliku oraz wiersz zawierający definicję funkcji strregerror w kodzie źródłowym programu sed. i grep "'strregerror' *.c m1sc.c:strregerror(errcode. pręg) W celu znalezienia zarówno definicji, jak i wszystkich przypadków jej użycia, należy po prostu użyć nazwy funkcji jako wyrażenia regularnego. S grep strregerror *.c compile.c: err(C0MPII.E. "RE error: Ss". strregerror(eval, *repp)); misc.c:strregerror(errcode. preg) process.c: errtFATAL. "RE error łs". strregerror(eval. defpreg)).

grep nie musi być stosowany względem kodu programu. Jeżeli nie jest się pewnym, czego dokładnie się szuka i gdzie zacząć poszukiwania, można użyć grep w celu prze­ szukania kodu z użyciem słowa kluczowego w nadziei, że zostanie ono wymienione w komentarzu lub będzie częścią identyfikatora. W celu zapewnienia, aby wielkość liter, końcówka wyrazu oraz jego odmiany nie ograniczały wyników wyszukiwania, należy je wykonywać przy użyciu samego rdzenia słowa. W poniższym przykładzie szukamy kodu związanego z procesem kompilacji. S grep ompil * c

[...] compile.c: O

netbsdsrc/sys.

p « compile_re(p. &a->u.r):

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

327

main.c: * Linked Ust pointer to compilatlon umts and pointer to main.c: compileO; main.c: * Add a compilatlon unit to the Hnked list

Narzędzia można również użyć w celu przeszukania danych wyjściowych innych na­ rzędzi. Przykładowo, w projekcie zawierającym wiele plików źródłowych można użyć grep do przeszukania listy plików pod kątem potencjalnie interesującychM. J Is | grep undo v_undo.c

Niekiedy nie chcemy czytać kodu. który zawiera wyrażenie regularne, gdyż jesteśmy zainteresowani tylko plikami, które je zawierają. Opcja -1 programu grep pozwala wyświetlić (raz) nazwę każdego pliku, który zawiera określone wyrażenie regularne. $ grep -1 xmalloc *.c *.h compile.c main.c misc.c extern.h

Można wówczas wykorzystać otrzymane dane w celu automatycznego wykonania określonego zadania na wszystkich plikach. Może tu chodzić o edycję każdego z nich w celu bliższego zbadania lub ich zablokowanie w ramach systemu kontroli wersji. W przypadku powłok uniksowych można tego dokonać, ujmując dane wyjściowe pro­ gramu grep w znakach tyldy. emacs 'grep -1 xmal1oc * c'

W innych środowiskach (na przykład Windows) należy zmodyfikować dane wyjściowe programu grep poprzez dodanie wywołania edytora na początku każdego wiersza i za­ chowanie wyniku w pliku wsadowym, który będzie można później wykonać. To i wiele podobnych zadań można wykonywać używając edycji strumieniowej (ang. stream editing). Pojęcie to odnosi się do zautomatyzowanego modyfikowania strumienia tekstu przez serię określonych poleceń. Dwa narzędzia powszechnie używane do edycji stru­ mieniowej to sed i Perl. Chociaż sed był projektowany z myślą o przetwarzaniu stru­ mieniowym. w prezentowanych przykładach będziemy używać języka Perl ze wzglę­ du na jego bardziej regularną składnię oraz możliwość edycji bezpośredniej. Jeżeli korzysta się z edycji strumieniowej w środowisku produkcyjnym, warto nauczyć się również obsługi programu sed, który może okazać się efektywniejszy w przypadku pewnych zadań. Najbardziej przydatnym poleceniem edycyjnym jest polecenie podstawienia, które określa się w sposób następujący: S/wyrażenie regularne/podstawienie i przełączniki

Polecenie to lokalizuje tekst pasujący do wyrażenia regularnego i zastępuje go tekstem podstawienia. W tym ostatnim można używać wyrażenia specjalnego $n w celu okre­ ślenia n-tej ujętej w nawiasy części wyrażenia regularnego (na przykład SI oznacza pierwszą ujętą w nawiasy część wyrażenia regularnego). Przełączniki to jednoznakowe parametry modyfikacji, które zmieniają zachowanie polecenia. Najbardziej przydat­ nym przełącznikiem jest g, który określa, że podstawienie ma zmieniać wszystkie nie netbsdsrc/usr. bin/vi.

328

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

nachodzące na siebie wystąpienia wyrażenia regularnego, a nie tylko pierwsze, co jest zachowaniem domyślnym. Używając programu Perl jako edytora strumieniowego, należy uruchomić go z opcją -p w celu określenia, że ma wykonać pętlę drukującą względem danych wejściowych oraz określić polecenie podstawienia jako argument opcji -e. Można określić więcej poleceń podstawienia, oddzielając je średnikami. Można również określić, że Perl powinien edytować plik w sposób bezpośredni, a nie działać jako filtr względem swoich danych wejściowych. Umożliwia to opcja -irozszerzenie-kopi i -zapasowej. Więcej szczegółów można znaleźć w dokumentacji Perlą. Wracając do naszego problemu utworzenia pliku wsadowego służącego do edycji pli­ ków, które pasują do wzorca — wszystko, co należy zrobić, to przekazać w ramach potoku dane wyjściowe programu grep do Perlą w celu zastąpienia początku każdego wiersza poleceniem edit. C :> g r e p -1 x m a l l o c * . c

| p e rl

-p -e

- s / A/ e d i t / '

> c o n t.b a t

C :> c o n t

|T] W przypadku powłoki uniksowej, dane wyjściowe Perlą można przekazać potokiem z powrotem do powłoki w celu ich natychmiastowego przetworzenia. g r e p -1 OBSOLETE * . c

| p e rl

-p -e

' s / A/ g z i p

/'

| sh

Powyższy kod kompresuje wszystkie pliki zawierające wyraz OBSOLETE. Można nawet przekazywać potokiem dane wyjściowe do pętli powłoki w celu wykonania wielu poleceń dla każdego dopasowanego pliku. Przykładowo, kod z listingu 10.3 używa programu grep w celu zlokalizowania wszystkich plików, które zawierają wyraz xmalloc i prze­ kazania potokiem danych wyjściowych do pętli do. Kod pętli wykonuje następujące działania: ♦ wczytuje nazwę każdego pliku jako zmienną $f; ♦ blokuje plik w repozytorium RCS, uniemożliwiając jego modyfikowanie; ♦ zmienia wszystkie wystąpienia identyfikatora xmal1 oc na safejnal loc, używając funkcji edycji bezpośredniej Perlą; ♦ zwalnia blokadę pliku w repozytorium, wstawiając do dziennika odpowiedni komentarz. Listing 10.3. Zmiana nazwy identyfikatora w ramach systemu RCS___________________________ grep -1 wnalloc *.C * .h | # --------------------------------------------Wyświetlenie plików zawierających wywołanie funkcji xmal loc Whlle read f • — Pętla wczytująca nazwy plików do zmiennej Si

d° CO -1 S11#



Zarezerwowanie pliku do edycji

perl -p -1 bak -e 'S/xmallOC/Safe malloc/g’ Sf • — Wykonanie podmiany bezpośredniej

ci

nTZnrieniono xmalloc na safe mai loc" -u S f# — Zwolnienie pliku

done

A W przypadku automatyzowania zadań z użyciem danych wyjściowych programu grep, należy pamiętać, że nie wyświetla on nazwy pliku, w którym znaleziono dopasowanie, jeżeli jako argument podano tylko jeden plik. Jeżeli używany skrypt wymaga określenia nazwy pliku, jego wykonanie zakończy się niepowodzeniem w przypadku uruchomienia [T| dla jednego pliku. Wersja GNU programu grep obsługuje opcję -with-fi lename, która

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

329

zapewnia, że nazwa pliku będzie zawsze wyświetlana. Na innych platformach można się przed tym zabezpieczyć, określając systemowy plik pusty (/dev/null w systemach uniksowych oraz NUL w systemie Windows) jako dodatkowy argument wywołania pro­ gramu grep. Plik ten nigdy nie będzie pasował do wzorca, ale wymusi na programie każdorazowe wyświetlanie nazwy pliku. Podobnie jak w przypadku wszystkich innych narzędzi operujących na poziomie leksy-

A kalnym, wyszukiwanie bazujące na wykorzystaniu programu grep nie zawsze pozwala na dokładne zlokalizowanie poszukiwanych elementów. W szczególności, grep nie potrafi sobie poradzić z podstawieniami dokonywanymi przez preprocesor C, elemen­ tami rozciągającymi się na więcej niż jeden wiersz, komentarzami oraz ciągami znaków. Na szczęście zazwyczaj wyszukiwania za pomocą programu grep kończą się szumem (niepotrzebne dane wyjściowe), a nie ciszą (wiersze, które powinny zostać uwzględnio­ ne, lecz nie są). Czasem można wyeliminować szum wyjściowy określając bardziej konkretne wyrażenie regularne. Przykładowo, jeżeli szukamy przypadków użycia funk­ cji ma Hoc, proste wyrażenie regularne spowoduje wyświetlenie następujących danych wyjściowych. $ grep malloc *.c *.h

[.. ] mlsc.c: 1f ((p - maltoc(size)) — NULL) tMsc.c: return (xmalloc(size)): mlsc.c: oe - xroalloc(s): extern.h:void *xmalloc P((u_1nt)):

W celu wyeliminowania niepotrzebnych wierszy z funkcją xmalloc, można określić w wyrażeniu regularnym, że pierwszym znakiem nie może być x, czyli [Ax]ma1 1 oc. Należy jednak zauważyć, że takie wyrażenie regularne nie dopasuje wystąpienia nazwy malloc na początku wiersza, ponieważ wówczas nie występuje znak początkowy, który można by dopasować do wzorca [*x]. Alternatywne, bardzo efektywne rozwiązanie, wymagające mniejszego wysiłku intelektualnego, polega na przefiltrowaniu danych wyjściowych programu grep w celu wyeliminowania niepotrzebnych dopasowań. Przy­ kładowo, wyszukanie przypadków użycia pola code struktury da następujące wyniki. $ grep code *.c *.h

[...] process.c: * This code is derived from software contributed to process.c: * 1. Redistributions of source code must retain the process.c: switch (cp->code) ( process.c: switch(cp->code) j defs.lv * This code is derived from software contributed to defs.h * 1. Redistributions o f source code must retain the defs.h: char code: /* Command code */

Opcja -v instruuje program grep, aby wyświetlał tylko te wiersze, które nie pasują do podanego wyrażenia regularnego, to znaczy, aby odfiltrował wszystkie wiersze pasu­ jące do niego. Zauważając, że wszystkie niepotrzebne wiersze stanowią fragmenty komentarzy blokowych rozpoczynające się od znaku *, możemy je wyeliminować w sposób następujący. $ grep code *.c *.h | grep -v "A\*"

[...] process.c: process.c: defs.h: char code;

switch (cp->code) { swltch(cp->code) { /* Command code */

330

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Używając funkcji edycyjnych powłoki, można zastosować to podejście w sposób przyro­ stowy. stopniowo oczyszczając dane wyjściowe programu grep za pomocą dodatkowych filtrów do momentu otrzymania dokładnie takiego rezultatu, jakiego się oczekuje. Niekiedy skomplikowane i nużące zadanie czytania kodu można zautomatyzować, łą­ cząc kilka narzędzi. Weźmy pod uwagę zadanie zlokalizowania w projekcie podzbioru wszystkich identyfikatorów zawierających znak podkreślenia (na przykład tylko tych, które są lokalne dla projektu). Takie zadanie można określić jako część procesu prze­ chodzenia do stylu kodowania, który wykorzystuje formatowanie identyfikatorów z uży­ ciem mieszanej wielkości liter (na przykład IdentifierFiłeName). Zadanie to można wykonać półautomatycznie w trzech etapach. 1. Tworzymy listę identyfikatorów kandydujących. 2. Ręcznie edytujemy listę w celu usunięcia nieodpowiednich identyfikatorów (na przykład nazw funkcji bibliotecznych i definicji typów). 3. Używamy listy jako danych wejściowych dla kolejnego przeszukania plików projektu. Najpierw lokalizujemy wszystkie wiersze zawierające takie identyfikatory. $ grep "[a-zO-9] [a-zO-9]" *.c *.h

[...] process c: process.c: process.c: process.c:

slze_t len: enum e_spflagspflag; slze_t tlen: struct s_command *cp. *end:

HI Następnie chcemy wyizolować identyfikatory, usuwając niepotrzebny kod. Można tego dokonać określając wyrażenie regularne Perlą, które odpowiada identyfikatorowi, i ujmując je w nawiasy w formie konstrukcji Perlą (wyrażenie). Następnie dla każdego wiersza są drukowane wyniki każdego takiego dopasowania ($ 1 ) po zastąpieniu dopasowanego PI wyrażenia ciągiem pustym. W ten sposób, w pętli, drukowane są wszystkie dopasowane identyfikatory. $ grep "[a-z0-9]_(a-z0-9]" *.c *.h | > perl -n -e 'while (s/\b(\w+_\w+)/ż) (print "$l\n"}'

[...] rm_so rm_eo rm_eo rm_so rm_so size_t e_spflag size_t s_command

Konstrukcja \b używana w wyrażeniach regularnych w przypadku Perlą wskazuje gra­ nicę wyrazu, \w wskazuje znak wyrazu, zaś + wskazuje jedno lub większą liczbę dopa­ sowali. Są to jedynie przydatne, skrócone formy zapisu prostych wyrażeń regularnych. P I Teraz chcemy zapewnić, aby każdy identyfikator występował tylko raz. Zwykle doko­ nuje się tego sortując dane wyjściowe (przy użyciu programu sort), a następnie usuwa­ jąc zduplikowane wiersze za pomocą programu uniq.

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

331

$ grep "[a-z0-9]_[a-z0-9]“ *.c *.h | » perl -n -e 'while (s/\b(W*_W*-)//> (print "Sl\n”}' > sort | > uniq >idl1st S cat idlist add_compun1t add_f1le cmd_fnits compile_addr compile_ccl compile_delimited

Po ręcznym przeedytowaniu listy w celu usunięcia identyfikatorów, które znajdują się poza zakresem projektu (na przykład składowe bibliotek języka C). otrzymujemy listę identyfikatorów, które należy wyszukać w plikach projektu. W tym celu można użyć narzędzia fgrep — związanego z programem grep. Wyszukuje ono jedynie stałe ciągi znaków (nie wyrażenia regularne) w plikach, ale może pobierać jako argument listę ciągów, względem których należy dokonać dopasowania. Algorytm używany przez program fgrep jest zoptymalizowany pod względem takich wyszukiwań — lista ciągów znaków może zawierać setki pozycji. Stąd fgrep jest przydatny w przypadku przeszu­ kiwania kodu źródłowego pod kątem wcześniej utworzonych list stałych ciągów zna­ ków. W naszym przypadku, zapisawszy poprawioną ręcznie listę identyfikatorów w pliku idl ist, możemy przeszukać wszystkie pliki. $ fgrep -f Idlist *.c compile.c: struct compile.c u_1nt compile.c: struct compile.c: 1nt

labhash *lh_next; lh_hash: s_coomand *lh_cmd; lh_ref:

[...] Kiedy kod źródłowy projektu zajmuje wiele katalogów, można zastosować kilka podejść. W przypadku prostej dwupoziomowej struktury katalogów można uruchomić program grep z odpowiednim wzorcem dopasowania nazw plików10. $ grep Isblank */*.c ex/ex_write.c: for (++p: *p && isblank(*p); ++p): ex/ex_write.c: for (p +- 2: *p && isblank(*p); ++p): vi/getc.c: 1f (csp->cs_flags !- O || !1sblank(csp->cs_ch))

Ponadto niektóre powłoki (takie jak zsh) pozwalają na określenie wyszukiwania rekurencyjnego za pomocą wzorca podobnego do poniższego.

**/* c W przypadku głęboko zagnieżdżonych hierarchii katalogów, najlepszym rozwiązaniem jest utworzenie listy plików, które chce się znaleźć, za pomocą polecenia find i przeka­ zanie otrzymanych wyników potokiem do polecenia xargs, określając jako jego argument grep oraz odpowiednie wyrażenie regularne". $ find . -name '*.c' -prlnt | xargs grep 'rmdlr(' ./rvfs/nfs_servc:nfsrv_rmdir(nfsd. slp. procp. mrq) ./ufs/ext2fs/ext2fs_vnops.c :ext2fs_rmdir(v) .ufs/1fs/1fs_vnops.c :1fs_rmd1r Cv)

10 nethsdsrc/usr. bin/vi.

11 netbsdsrc/sys.

332

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source ufs/lfs/lfs_vnops.c: ret - ufs_rmdir(ap). /ufs/ufs/ufs_vnops.c:ufsjmdi r( v)

[...] Polecenie fin d powoduje przejrzenie określonej hierarchii katalogów i wydrukowanie nazw plików, które spełniają określone warunki (w tym przypadku są to pliki, których nazwa odpowiada wzorcowi *.c). Polecenie xargs pobiera jako argument nazwę po­ lecenia oraz zestaw argumentów początkowych, odczytuje dodatkowe argumenty ze standardowego wejścia, a następnie wykonuje polecenie raz lub większą liczbę razy z użyciem argumentów początkowych oraz argumentów odczytanych ze standardowe­ go wejścia. W opisywanym przypadku xargs uruchamia program grep przekazując do niego jako argumenty nazw plików dane wyjściowe polecenia find, czyli wszystkie pliki *. c znajdujące się w katalogu bieżącym (.). Używając takiego schematu, program grep może przetworzyć dowolną liczbę plików bez ograniczeń co do systemowych obostrzeń względem rozmiaru argumentów podawanych w wierszu poleceń. Jeżeli środowisko systemu Windows nie wspiera takiej konstrukcji, trzeba wykazać się większą kreatyw­ nością i dynamicznie skonstruować tymczasowy plik wsadowy zawierający potrzebne polecenia. Dyskusję na temat narzędzi typu grep zakończymy wzmianką o trzech przydatnych opcjach wywołania tego programu. ♦ Przeszukanie wszystkich komentarzy i kodu języków o identyfikatorach niezależnych od wielkości liter (na przykład Basic) przy użyciu wzorca dopasowania bez uwzględnienia wielkości liter (grep -i), ♦ Wyrażenie regularne, które rozpoczyna się od myślnika, jest błędnie interpretowane przez program grep jako przełącznik. W celu uniknięcia tego problemu, dane wyrażenie regularne należy określić przy użyciu przełącznika -e. ♦ Używając przełącznika -n można utworzyć listę plików oraz numerów wierszy, które odpowiadają danemu wyrażeniu regularnemu. Kiedy objętość danych, jakie chcemy przeszukać za pomocą programu grep. staje się bardzo duża, warto rozważyć wykorzystanie innego rozwiązania. W podrozdziale 10.2 omówiono narzędzia, które tworzą bazę danych znaczników dla popularnych edytorów. Poza tym, jeżeli edytowanie i przeglądanie kodu źródłowego nie jest głównym celem działań, można rozważyć użycie narzędzia indeksującego ogólnego przeznaczenia, takiego jak Glimpse12. Ćwiczenie 10.8. Programiści często oznaczają fragmenty kodu, które wymagają pew­ nych działań z ich strony, używając specjalnego znacznika, takiego jak XXX lub FIXME. Wyszukaj, policz i zbadaj takie przypadki w kodzie źródłowym zawartym na płycie dołączonej do książki. Ćwiczenie 10.9. Zadanie lokalizowania identyfikatorów zawierających znak podkreśle­ nia było związane z ręczną edycją listy wyjściowej w celu usunięcia identyfikatorów, które stanowią część bibliotek systemowych (na przykład size_t). Zaproponuj sposób 12

http:/fwebglirnpse. org/.

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

333

zautomatyzowania tego zadania, biorąc pod uwagę fakt, że identyfikatory używane przez biblioteki systemowe są deklarowane w plikach dołączanych, które można przetwarzać. Ćwiczenie 10.10. Deklaracje funkcji systemowych występujące w dystrybucji systemu uniksowego opisywanego w niniejszej książce są generowane na podstawie jednego pliku13. Zlokalizuj definicje pierwszych 20 funkcji systemowych w kodzie źródłowym. Maksymalnie zautomatyzuj wykonanie tego zadania. Ćwiczenie 10.11. Napisz skrypt powłoki lub plik wsadowy wykorzystujący program grep, wyrażenia regularne i inne narzędzia w celu utworzenia listy naruszeń standardu kodowania w zbiorze plików źródłowych. Utwórz dokumentację znalezionych naru­ szeń oraz rodzajów błędnie dopasowanych fragmentów kodu.

10.4. Znąjdowanie różnic między plikami Kusząco łatwym sposobem ponownego wykorzystania kodu jest utworzenie kopii od­ powiedniego kodu źródłowego i jego zmodyfikowanie zgodnie z bieżącymi potrzebami. Z takim sposobem wielokrotnego używania kodu jest jednak związanych wiele proble­ mów. Jeden z nich polega na tym, że tworzone są dwie oddzielne wersje bazy kodowej. Innym częstym przypadkiem występowania dwóch różnych wersji plików jest badanie procesu rozwoju kodu. W obu przypadkach w końcu otrzymuje się dwie nieco różniące |T1 się od siebie wersje pliku źródłowego. Jednym ze sposobów ich porównania jest wy­ drukowanie listingów i ich zestawienie. Bardziej efektywnym rozwiązaniem jest jednak użycie odpowiedniego narzędzia. Program d iff porównuje dwa różne pliki lub katalogi i tworzy listę wierszy, które należy zmienić w celu ujednolicenia obu plików. Narzędzie d iff potrafi na wiele różnych sposobów przekazać na wyjście informacje o istniejących różnicach. Niektóre formaty wyjściowe są zwięzłe i zoptymalizowane do użycia przez inne programy, takie jak ed, RCS lub CVS. Jeden z formatów, context diff, jest szczególnie przyjazny dla użytkownika: wyświetla różnice występujące między dwoma plikami w kontekście sąsiednich wierszy. Opcja -g programu diff określa format wyjściowy. Inne narzędzia, takie jak CVS i RCS (patrz podrozdział 6.5), również obsłu­ gują tę opcję w przypadku porównywania różnych wersji kodu źródłowego. Wiersze, które są różne, zostają oznaczone symbolem !, wiersze, które dodano, oznaczone sym­ bolem +, zaś wiersze usunięte — symbolem -. Na listingu 10.4 przedstawiono przykład danych wyjściowych programu diff. Dwa porównywane pliki są różnymi wersjami kodu sterownika karty sieciowej — jeden plik stanowi część interfejsu sieciowego systemu operacyjnego14, zaś drugi jest używany w celu uruchamiania systemu przez sieć1'.

13 netbsdsrc/sys/kern/syscalls. master. 14 netbsdsrc/sys/arch/i386/stand/lib/netif/wd80x3.c: 364 - 382. 15 netbsdsrc/sys/arch/i386/netboot/wd80x3.c\ 331 - 346.

334

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Listing 10.4. Porównywanie plików: dane wyjściowe polecenia diff -c *** stand/11b/net1f/wd80x3 c Wed Jan 07 02:45:48 1998»----- netboot/wd80x3.c Tue Mar 18 01:23:53 1997

*** 364.382 *****-------------------------------------* available. If there is. Its contents 1s returned in a * pkt structure, otherwise a nil pointer 1s returned.

-Porównywane pliki a i b

- Plik a - Wiersze w pliku a

*/ 1 I I !

1nt • ----------------------------EtherRecelvelpkt. maxlenl char *pkt: 1nt maxlen:

■Wiersze zmienione

u_char pageno. curpage. nextpage: int dpreg - dpc dc_reg: dphdr_t dph; u_long addr: 1f (mb(dpreg ♦ DP_RSR> & RSR.PRX) { intlen:*-----------------------------------------

■Wiersze usunięte z pliku a

/* get current page numbers */ pageno - mbidpreg ♦ DP BNRY) + 1; if (pageno — dpc.dc_pstop)

m

331.346 -------------------------------------* available, if there 1s. Its contents 1s returned In a * pkt structure, otherwise a nil pointer is returned

-P lik b -Odpowiednie wiersze w pliku h

*/ packet_t * * --------------------------------EtherRecelve(void). { u_char pageno. curpage. nextpage; int dpreg - dpc.dc_reg: packetj *pkt;*----------------dphdr t dph: ujong addr: pkt - (packet t *)0:*-------------1f n d r e t x i d « Oxfff, o w p - > n d retxid 1583 L I S T B E H O V E(nfsd, n d hash); 1584 L I S T REHOVE(nfsd, nd tq) ; 1585 if (o»p->nd e o f f < n f s d - > n d eoff) < 1586 o v e r l a p * o v p - > n d e o f f - n f s d - > n d off; 1587 if (overlap < 0) 1588 p a n i c ( " n f s r v c o a l e s c e : b a d o f f ”) ; 1589 if (overlap > 0)

M M ÊÊÊ



1

Connect: looking up host* bd Inux no . g

_____

A

J

ń

jT

a

&

3

£

6

Ponadto możliwości przeglądania kodu oferuje również większość zintegrowanych środowisk programowania. Pracując nad dużym wieloplikowym projektem warto czasem załadować go właśnie jako projekt w ramach środowiska — nawet jeżeli nie zamieiza się z niego korzystać w celach programistycznych — aby móc skorzystać z możliwości przeglądania kodu. O ile przeglądarki kodu źródłowego umożliwiają nawigowanie po dużych kolekcjach kodu źródłowego, o tyle upiększacze oraz narzędzia typu p retty-p rin ter mogą pomóc w odkryciu szczegółów dotyczących kodu. Upiększacze, takie jak uniksowe programy cb lub indent , dokonują ponownego formatowania plików z kodem źródłowym w celu zapewnienia zgodności z określonymi konwencjami formatowania kodu. Programy te 22 23

http://cscope.sourceforge.net/. http://cbrowser.sourceforge.net/.

24 http://sources. redhat. com/sourcenav/. 25 http://bcr.linux.no/.

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

345

obsługują wcięcia, umiejscowienie nawiasów, białe znaki wokół operatorów oraz słowa kluczowe, deklaracje i końce wierszy. Można określić opcje wywołania z poziomu wiersza poleceń i utworzyć pliki konfiguracyjne służące do dostosowania zachowania A programu do lokalnych konwencji formatowania. Nie należy ulegać pokusie dostoso­ wywania obcego kodu do własnych standardów kodowania — nieuzasadnione zmiany formatowania tworzą różne bazy kodu i utrudniają zorganizowane zarządzanie kodem. Obecnie większość kodu jest pisana przez kompetentnych profesjonalistów i jest zgod­ na z zestawem reguł formatowania. Upiększacze działają automatycznie i często mogą zniszczyć starannie opracowane formatowanie, które daje dodatkowe wskazówki od­ nośnie do znaczenia kodu. Jednakże istnieją pewne sytuacje, w których użycie upięk[7] szaczy jest uzasadnione. Należy je wykorzystywać w celu: ♦ poprawiania kodu, który zapisano niezgodnie z jakimikolwiek standardami formatowania; ♦ dodawania kodu w celach konserwacji; ♦ utworzenia tymczasowej wersji kodu w celu ułatwienia jego zrozumienia; ♦ zintegrowania kodu w ramach większego projektu; Narzędzie typu pretty-printer w elegancki sposób składa kod źródłowy programu w celu zwiększenia jego czytelności. Jeden z często używanych stylów — stosowany przez uniksowy program vgrind — każe pisać komentarze kursywą, słowa kluczowe — pogrubieniem oraz umieszczać na marginesie każdej strony nazwy odpowiednich funk­ cji, których kod znajduje się obok. Na rysunku 10.5 przedstawiono fragment kodu pro­ gramu rm d irb złożonego za pomocą narzędzia vgrind. Czytając kod dostępny w wersji elektronicznej, można skorzystać z funkcji różnokolo­ rowego podświetlania składni, którą oferuje wiele współczesnych edytorów i zintegro­ wanych środowisk programowania w celu wyróżnienia elementów charakterystycznych dla różnych języków. Należy pamiętać, że edytory i programy typu pretty-printer często pozwalają na określenie kluczowych elementów składni języka, tak aby można było je dostosować do dowolnego języka, w jakim się pracuje. W pewnych przypadkach konieczne może okazać się sięgnięcie po bardziej wyspe­ cjalizowane narzędzia. Jeden z często występujących przypadków dotyczy deklaracji w języku C: połączone użycie operatorów przedrostkowych i przyrostkowych w celu zadeklarowania typu może spowodować, że pewne deklaracje będą wyglądać na nie­ zrozumiałe. Program cdecl2 tłumaczy takie deklaracje do postaci zdań zapisanych w języku angielskim. Weźmy pod uwagę poniższą definicję2 . int (*elf_probe_funcs[])() - {

[...] }: Prosta operacja kopiuj-wklej treści definicji do programu cdecl daje natychmiast wyja­ śnienie2 . "6 netbsdsrc/bin/rmdir/rmdir.c: 1-134. 27

" ftp://metalab.unc.edu/pub/linux/devel/lang/c/cdecl-2.5.tar.gz.

28 ?9

netbsdsrc/sys/arch/mips/mips/elf.c: 62 - 69. Zadeklaruj elf_probe_funcsjako tablicę wskaźników na funkcję zwracającą wartość typu in t—przyp. tłum.

346

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rysunek 10.5.

Dokument z kodem źródłowym programu rmdir złożony za pomocą narzędzia vgrind



~

n n d ir .c

r m d ir .c

/* łN ftB S D : nndir.c.v 1.14 IW 7 A J 7 /2 0 20:52-05 ch ru to s Esp S /• u •/ »include < en\h> »include •in clu d e »include •in clu d e >-_smng.h> •in clu d e •in clu d e

•/

Int rm _paih __ P t(c h u r •)); void usage __P«*old)); Int main __Pi (Int. c h a r *1|)); in t m ainfargc. arg>) Int arge: c h a r * a rg \||. ii Int ch. c m n ; Int pflag.

m a in

seOocale !* - U sw itch ich) | case 'p \ pflag - 1; bre ak : case T : d e fau lt usage«); arge - 3 optind; argv *= opund: If (arge = 0) usage«); fo r (errors • 0; *argv; argve-e) ( c h a r *p; / • D elete trailing th u h t t . p e r POSIX. • / p = *argv ♦ sirlcnl *argv); w hile (— p > *arg> && *p = '/r ) •++ p ■ VT; If (rm dir(*argv) < 0 ) ( w a rn C * » ', ’argv); cttoo a I; 1 d s c If (pflag) errors (= rm path«*aigv); 11 cxit(enors). \ r

l~ 1 • /

void usage«) 1

u sa g e

(void)fpnnlf(stdc(T. ‘ usage: rm dlr |- p ) directory ...Vit"); cxiK I); )

A p r 2 0 1 1 :2 3 2 0 0 1

Page / o f nndir.c

cdec1> explain int (*elf_probe_funcs[])0 declare elf_probe_funcs as array of pointer to function returning int

Tego samego programu (w jego odmianie c++decl) można użyć w przypadku typów języka C++30’31.

3'’ace/apps/drwho/PMC_Ruser. cpp: 119. 11 Zadeklaruj get_name jako wskaźnik na składową klasy Drwho_Node funkcji pobierającej argument typu

void, (czyli bezparametrowej) zwracającej wskaźnik na stalą typu char — przyp. tłum.

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

347

c++dec1> explain const char *(Drwho_Node:.*getjiame)(void) declare getjiame as pointer to member of class Drwho_Node function (void) returning pointer to const char

Ćwiczenie 10.23. Wypróbuj swoje zintegrowane środowisko programowania wzglę­ dem kodu źródłowego jądra systemu uniksowego32. Wymień i skomentuj napotkane trudności. Ćwiczenie 10.24. Czy przeglądarka kodu używanego przez Ciebie zintegrowanego środowiska programowania potrafi współpracować z innymi narzędziami? Na przykład, czy istnieje możliwość przeszukiwania lub przetwarzania w inny sposób wyników pre­ zentowanych przez przeglądarkę albo czy potrafi ona automatycznie tworzyć raport definicji każdej występującej funkcji? Zaproponuj projekt przeglądarki kodu źródło­ wego, która zapewni taką elastyczność. Ćwiczenie 10.25. Napisz narzędzie, które odczytuje plik źródłowy języka C i określa zastosowane w nim konwencje formatowania, generując odpowiednie przełączniki dla uruchomienia programu indent. Ćwiczenie 10.26. Sformatuj jeden z plików przykładowych za pomocą narzędzia typu pretty-printer. Ćwiczenie 10.27. Sprawdź, czy Twój edytor obsługuje definiowane przez użytkownika kolorowanie składni. Zdefiniuj taki schemat kolorowania dla języka, którego twój edytor nie obsługuje. Ćwiczenie 10.28. Zintegruj narzędzie cdecl z ulubionym zintegrowanym środowiskiem programowania.

10.8. Narzędzia używane w czasie uruchomienia Wartościowy wgląd w sposób działania programu można często uzyskać poprzez jego uruchomienie. Jest tak szczególnie w przypadku programów, którym brak odpowiedniej dokumentacji lub których dokumentacja jest nieaktualna. W takim przypadku, zamiast próbować zrozumieć kod wiersz po wierszu, można uruchomić go dla danych testowych i obserwować zewnętrzne przejawy działania. W celu otrzymania bardziej szczegółowego obrazu sytuacji, można zbadać, w jaki spo­ sób program współpracuje z systemem operacyjnym. Ze względu na fakt, że wszyst­ kie zasobu programu są kontrolowane przez system operacyjny, obserwowanie takiej współpracy zapewnia wartościowy wgląd w kwestie związane z jego funkcjonalnością. Platformy wielu systemów operacyjnych wspierają narzędzia, które monitorują i wy­ świetlają wszystkie funkcje systemowe wywoływane przez program. Do takich narzędzi 32

netbsdsrc/sys.

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

348

należą trace w systemie MS-DOS (listing 10.9), API Spy dla systemu Microsoft Win­ dows (rysunek 10.6) oraz stracę dla systemu Unix (listing 10.10). Dziennik wywołań systemowych zawiera wszystkie pliki, które program otwiera dla odpowiednich danych wejściowych i wyjściowych, wartości zwracane przez wywołania systemowe (w tym błędy), informacje o wykonaniu innych procesów, obsłudze przerwali, działaniach wy­ konywanych na systemie plików oraz operacjach dostępu do systemowych baz danych. Ponieważ nie trzeba przygotowywać w specjalny sposób programu do monitorowania wywołań systemowych, takie rozwiązanie można wykorzystać w przypadku progra­ mów, do których kodu źródłowego nie posiada się dostępu. Listing 10.9. Dane wyjściowe programu trace w systemie MS-DOS___________________________ 20:53:05 20:53:05 20 53:05 20 53:05 20:53:05 20:53:05

27b5 30 27C5:00C2 get version O - 7.10 27b5 4a 27C5.0137 realloc(27B5.0000. 0x11320) - ok 27b5 30 27C5:03AE get verslonO - 7.10 27b5 35 27C5:01AD get vector(0) - 1308:2610 27b5 25 27C5:01BF set vector(0. 27C5:0178) 27b5 44 27C5:0254 loctl(GET DEV INFO. 4) CHARDEV: NOT EOF LOCAL NO I0CTL



tm cjataacja programu

[...]

20:53:05 27b5 44 27C50254 loctl(GET DEV INFO. 0) CHARDEV. STD1N STD0UT SPECIAL NOTJOF LOCAL CANJ0CTL 20:53:05 20 53:05 20:53:05 20-53:05

27b5 27b5 27b5 27b5

40 27C5.0FDA writed. 40 27C5:0FDA writed. 40 27C5:0FDA writed. 40 27CS:OF80 writed.

28E7:1022. 5) * 5 28E7:0D6C. 1) - 1 28E7:1022 . 51 - 5 28E7:0B4C. 2) - 2

"hello"»— Faktyczne wykonanie "world" "\r\n"

20:53:05 27b5 25 27C5:030E set vectorfO. 1308:2610)»------------- Funkcje kończące 20:53 05 27b5 4c 27C5.02F3 exit(O)

R y su n ek 1 0 .6 .

Program Microsoft Windows API Spy

^ APIS3? v. ? .5

U N R FG ISrtR fD - rm p a in l.e x e

01031580: G etJloduleH andleA (LPSTR:00000000) 0 1 0 3 1 5 8 6 :G etH oduleH andleA • 1000000 ( n s p a i n t.e x e ) 0 1 0 1 0 2 1 3 :Ce tCoamand LineW() 0 1 0 1 0 2 1 9 :GetCoanandLineW - 20668 ( m s p a in t.e x e ) 0 1 0 3 4 E 6 0 :L o a d L lb ta ry A ( LPSTR:0 1 0 0 7 5 6 0 :"UxThene. d l l - ) 01034E66*. L o ad L lb tary A - 5B100000 ( m s p a in t.e x e ) 010096EA: v s p r in tC V ( LPtfSTR:0006FB98,LPWSTB:0 1 0 0 2 1 9 8 :"%d-%d") 010096F0: v s p n n t f V » 6 (m s p a in t.e x e ) 010096EÀ:w s p r in tf V ( LPtfSTR:0006FB88, LPVSTP:0 1 0 0 2 1 9 8 :-%d-%d")

Log Mane jc: \W IH I> 0W 3\sy3ta»32\n3paint. Log S t a t u s : T h re a d te r m in a te d

O p tio n s

O slS E Z )

Change

txpsr

2av e LOG I

C le a r

I

to n e

E& it

L isting 1 0 .1 0 . Dane wyjściowe programu stracę w systemie Linux execve(”/b1n/echo". ["echo”, "hello", "world"]. [/* 22 vars */] - 0 brk(0) • 0x804,i668

• --- Imcjahzacja

[...] getpldd brk(O) brk(0x804a808)

- 652 - 0x804a668 - 0x804a808

[...] open("/usr/share/locale/en US/LC MESSAGES/SYS LC MESSAGES'. 0 RD0NLY) - 4 fstat(4. {st roode-S 1FREG|0644, st s1ze-44. ...))- 0 mmap(0. 44. PR0T READ. MAP PRIVATE 4. 0) - 0x40013000 close(4) -0

programu

349

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

WfiteCl. "hello world\n". 12) munmap( 0x40017000. 4096) _ex1t(0)

• 12• ■ 0# - ?

Faktyczne wykonanie

~

Zakończenie programu

Granica systemu operacyjnego to nie jedyne miejsce, w którym można monitorować działania programu. Dwa inne źródła to sieć oraz interfejs użytkownika. Wiele progra­ mów, takich jak tcpdump oraz WinDump, potrafi monitorować i wyświetlać pakiety sieciowe. Ograniczając monitorowanie do określonego hosta oraz portu TCP, można otrzymać dokładny obraz działania programu z punktu widzenia sieci. W ten sposób można obserwować protokół komunikacyjny w czasie jego działania, zamiast próbo­ wać wydedukować jego funkcjonalność na podstawie kodu źródłowego. Kolejna klasa programów pozwala na badanie programu na poziomie interfejsu użytkownika. Program Xev systemu XWindow oraz Spy++ systemu Windows monitoruje zdarzenia przechwyp~l tywane przez okna i wyświetla je w czytelnej formie. Interaktywne programy okien­ kowe są często kodowane w postaci pętli zdarzeń — określone działania użytkownika można dopasować do odpowiedniego kodu obserwując zdarzenia generowane przez te działania. Program profilujący (ang. execution profiler) umożliwia uzyskanie informacji na róż­ nym poziomie szczegółowości. Mechanizm ten, wbudowany w zintegrowane środowi­ ska programowania takie jak Microsoft Visual Studio lub dostępny w formie narzędzia gp ro f w systemach uniksowych, monitoruje działanie programu i udostępnia odpowied­ ni profil. Zawiera on zwykle listę przedziałów czasu spędzonego na wykonywaniu każ­ dej funkcji (oraz, opcjonalnie, jej funkcji podrzędnych). Większość programów profi­ lujących potrafi również obliczać dynamiczny graf wywołań (ang. call graph) programu. Ilość czasu spędzonego w każdym podprogramie może pomóc w szybkim określeniu obszarów kodu, które mogłyby dużo zyskać dzięki optymalizacji. Używając grafu wy­ wołań, można poznać strukturę programu oraz interakcje zachodzące między podpro­ gramami. Jeżeli dane testowe będą wystarczająco bogate, można nawet znaleźć tzw. martwy ko d (ang. dead code), czyli kod, który nigdy nie jest wykonywany. W celu aktywowania zbioru danych o profilu, zwykle należy skompilować kod z odpowiednią opcją kompilatora i skonsolidować go ze specjalną biblioteką. Po zakończeniu progra­ mu plik będzie zawierał dane o profilach w nieprzetworzonym formacie. Następnie generator raportów przetworzy je w celu otrzymania końcowej postaci profilu. Jeszcze dokładniejszy profil wykonania można otrzymać, wykorzystując technikę no­ szącą nazwę zliczania wierszy (ang. line count) lub analizy pokrycia bloków podstawo­ wych (ang. basic block coverage analysis). W tym przypadku przełącznik kompilatora (dla gee jest to -a w połączeniu z -pg oraz -g) lub procedura przetwarzania kodu obiektu (w przypadku Microsoft Visual Studio — prep) tworzy kod, który zlicza liczbę wyko­ nali każdego bloku podstawowego. Bloki podstawowe to sekwencje instrukcji o dokładnie jednym punkcie wejścia i wyjścia. Stąd, zliczając liczbę wykonań każdego bloku pod­ stawowego, można przekazywać wynik do pojedynczych wierszy kodu źródłowego. Szukając nigdy nie wykonywanych wierszy, można znaleźć słabości w danych testo­ wych i odpowiednio je zmienić. Poznanie liczby wykonań każdego wiersza pozwala na odkrycie sposobu funkcjonowania algorytmu.

350

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Weźmy pod uwagę przykład z listingu 10.11. Jest to część listingu programu gprof opatrzonego uwagami, wygenerowanego poprzez uruchomienie programu we3'1 (służą; cego do zliczania znaków, wyrazów i wierszy) względem jego kodu źródłowego. Wy­ korzystano opcje programu g p ro f 1, -A oraz - x w celu wygenerowania określonych danych wyjściowych. Kod źródłowy składa się z 269 wierszy, 968 wyrazów i 6149 znaków. Wartości te bezpośrednio przekładają się na liczbę powtórzeń określonych wierszy kodu źródłowego (listing 10.11:3. 10.11:4 oraz 10.11:2). Równie interesujący jest sposób, w jaki oznaczono pętle: trzy liczby występujące na początku wiersza (listing 10 . 1 1 : 1 ) określają liczbę wejść do pętli, liczbę przejść na jej początek oraz liczbę wyjść z pętli. Warto zwrócić uwagę, w jaki sposób liczba powrotów pętli while na jej początek (3) odzwierciedla liczbę wykonań jej treści (2). Wiersze, które nigdy nie są wykonywa­ ne, również oznacza się w specjalny sposób (listing 10.11:5), co pozwala na zidentyfi­ kowanie części programu, które nie brały udziału w przetworzeniu określonych danych wejściowych. W czasie pisania niniejszej książki dane wyjściowe z profilowania blo­ ków podstawowych generowane przez program gee nie były kompatybilne z danymi pobieranymi przez program gprof. W celu sformatowania takich danych do odpowied­ niego formatu, można użyć niewielkiego skryptu języka Perl (bbconv.pl — stanowi on część dystrybucji pakietu gprof). Listing 10.11. Profilowanie bloków podstawowych z a pomocą n a rzęd zia gprof_______________ 1 -> gotsp - 1; 1 -> while ((len______ 1.3.1 (den - readffd. buf. HAXBStZE>) > 0) ( I— [1] Licznik wykonań pytli while 2 2j^>> charct charct ++- len: len~ ~ 2.6151. 2 ^>> for ++C) { ^----------- (!] Licznik wykonań pętli fo r for (C (C -- buf: buf: len--: 1 6149 -> 1f 1487 269 -> 1487 4662 968 968

gotsp - 1; 1 f ( * C ~ ' \ n ,){ ++1 Inect ; • ----------[3] IVvkonvwane dla kaidego wiersza

)

-> ->

) else { if (gotsp) ( gotsp • 0 :# -------- [4J Wykonywane dla każdego wyrazu ++wordct.

->

•>

2 -> ) 1 -> ##### ->

#### ->

)

1

if(len ~ -1) ( warn( m%Sm. rval - 1:

)

f i l e ) ; • ------------------------------------- [5] Nigth’ nie w ykonw ane wiersze

W celu zbadaniakażdego szczegółudynamicznego działania programu, który podlega badaniu, należy go uruchomić pod kontrolą debuggera. Choć służy on głównie do znaj­ dowania błędów, jest również bardzo wszechstronnym narzędziem w zakresie bada­ nia działania programu. Poniższa lista opisuje najbardziej przydatne cechy debuggera związane z czytaniem kodu. ♦ Wykonywanie program u krok po kroku pozwala na dokładne prześledzenie sekwencji operacji wykonywanych przez program dla określonych danych wejściowych. Większość debuggerów pozwala na przeskakiwanie wywołań podprogramów (kiedy określony podprogram nas nie interesuje) lub wchodzenie w wywołania (kiedy chce się poznać działanie podprogramu). 33

netbsdsrc/usr.bin/wc/wc.c: 201 -228.

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

♦ Pułapki w kodzie dają możliwość zatrzymywania programu w momencie osiągnięcia określonego miejsca. Można ich używać w celu szybkiego przechodzenia do odpowiednich miejsc lub sprawdzenia, czy dany fragment kodu jest wykonywany. ♦ Pułapki dla danych dają możliwość zatrzymania wykonywania programu w momencie, gdy zostanie odczytany lub zmodyfikowany określony element danych (na przykład zmienna). Przy użyciu wsparcia sprzętowego oferowanego przez współczesne procesory, ta niedoceniana funkcja pozwala w wydajny sposób monitorować sposoby uzyskiwania dostępu do danych w programie. ♦ Wyświetlanie zmiennych zapewnia dostęp do wartości zmiennych. Pozwala to sprawdzić, w jaki sposób się one zmieniają w czasie wykonywania programu. ♦ Z rzut stosu oferuje możliwość poznania historii wywołań, które doprowadziły do bieżącego miejsca wykonania. Zrzut stosu zawiera każdy podprogram wraz z jego argumentami (poczynając od funkcji main w programach języka C i C++). ♦ Przeglądanie struktury pozwala na zwijanie i rozwijanie struktury elementów oraz badanie i weryfikowanie struktur danych. Debuggery to kolejna klasa narzędzi, które znacznie zyskują na interfejsie graficznym. Jeżeli Czytelnik korzysta z debuggera w trybie tekstowym, powinien poważnie rozwa­ żyć przejście na wersję graficzną. Ćwiczenie 10.29. Uruchom program pod kontrolą monitora wywołań systemowych i przeanalizuj wykonane wywołania systemowe. Nieinteraktywne programy zwykle generują mniej szumu niż interaktywne. Ćwiczenie 10.30. Wygeneruj dane o profilu wykonania programu p rin tf4. Używając tych danych (bez przeglądania kodu źródłowego), narysuj schemat struktury programu (sposoby wzajemnego wywoływania podprogramów). Ćwiczenie 10.31. Skompiluj program implementujący sortowanie szybkie3' z akty­ wowanym profilowaniem zliczania wierszy. Napisz prosty program sortujący wiersze (system testowy) i zbadaj liczniki wykonania wierszy w przypadku danych losowych oraz danych posortowanych w porządku rosnącym lub malejącym. Można użyć uniksowego programu jo t w celu utworzenia sekwencji losowych danych oraz programu sort w celu ich posortowania. Czy otrzymane wielkości są zgodne z oczekiwaniami? Ćwiczenie 10.32. Utwórz opis sposobów wykorzystania opisanych funkcji debuggerów w Twoim środowisku programowania.

10.9. Narzędzia nieprogramowe Naszą dyskusję zakończymy omówieniem narzędzi, które można wykorzystać w celu czytania kodu, a które nie są wynikiem działań programistów. 34 netbsdsrc/usr. bin/printf/printf.c. 35 netbsdsrc/lib/libc/stdlib/qsort.c\ 1-182.

352

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Kod, który trudno zrozumieć warto wydrukować na papierze. Typowy monitor wy­ świetla obraz składający się z około miliona punktów, natomiast strona wydrukowana na drukarce laserowej może się składać z ponad 30 milionów punktów. Ta różnica oznacza zawarcie większej ilości informacji na stronie oraz mniejsze obciążenie dla wzroku. Co ważniejsze, wydruk można zabrać ze sobą do miejsc, w których można się skoncentrować bardziej niż za biurkiem, można też z łatwością wyróżniać kod, rysować linie oraz zapisywać uwagi na marginesie. Należy używać markerów, karteczek samo­ przylepnych i wszelkich innych elementów, które mogą pomóc w zrozumieniu kodu. Warto również rysować schematy ilustrujące działanie kodu. Wydruk pozwala na odej­ ście od monitora, więc teraz można na kartce papieru narysować schematy dla elemen­ tów, które chce się zrozumieć. Nie należy dążyć do otrzymania idealnych czy w ogóle poprawnych schematów — akceptowalne jest wszystko, co pomaga lepiej zrozumieć czytany kod. Poniżej wymieniono elementy, które mogą się znaleźć na schemacie. ♦ Funkcje w niewielkich ramkach ze strzałkami określającymi wywołania lub przepływ danych. ♦ Hierarchie klas lub diagramy konkretnych instancji obiektów z odpowiednio określonymi właściwościami. ♦ Struktury danych (drzewa, listy itd.) przy użyciu strzałek określających wskaźniki. ♦ Pola bitowe oraz odpowiednie maski rejestrów odwzorowań bitów. ♦ Diagramy przejść stanów, na których kółka określają stany, zaś strzałki — przejścia stanów. ♦ Ciągi znaków lub tablice oraz odpowiednie indeksy lub wskaźniki. ♦ Diagramy związków encji umożliwiające zrozumienie dopasowania do siebie części większego systemu. Należy używać ołówka i być przygotowanym na konieczność wprowadzania poprawek w miarę coraz lepszego poznawania działania programu. Lepsze zrozumienie fragmentu kodu można uzyskać, wyjaśniając go komuś innemu. Zrozumiałe omówienie działania programu innej osobie wymaga przemyślenia pro­ blemu na nieco innym poziomie i pozwala dostrzec wcześniej przeoczone szczegóły. Ponadto, w celu poprawnego wyjaśnienia programu często trzeba sięgnąć po materiały dodatkowe, takie jak książki, schematy oraz wydruki, z których z lenistwa nie skorzy­ stało się wcześniej. W celu zrozumienia skomplikowanego algorytmu lub wyrafinowanej struktury danych, należy wybrać spokojne i ciche otoczenie i skoncentrować się na nim bez sięgania po pomoc komputera. Dijkstra w jednym ze swoich wykładów szczegółowo opisał, jak osoby poproszone o wyjaśnienie działania określonego algorytmu synchronizacji nie­ zmiennie stawały przed przerastającym je problemem w momencie, gdy w ich rękach znalazł się ołówek lub długopis [DijOl]. Podobnie negatywny wpływ mogą mieć prze­ rwy. Psychologowie używają pojęcia przepływ (ang.flow) na określenie głębokiego, wykazującego niemal cechy medytacji zaangażowania w określone zadanie, często związanego z euforycznym stanem umysłu oraz niepostrzeganiem upływu czasu. Jest to stan, w którym należy się znaleźć, chcąc zrozumieć skomplikowany algorytm. Niestety,

Rozdział 10. ♦ Narzędzia pomocne w czytaniu kodu

353

jego osiągnięcie może wymagać nawet 15 minut i wszelkie przerwy (telefon, nowa wiadomość pocztowa, kolega proszący o pomoc) powoduje, że wszystko trzeba zaczy­ nać od początku. Dlatego też należy zapewnić sobie warunki pracy, które w przypadku, gdy to konieczne, pozwolą uniknąć tego rodzaju przerw. Ćwiczenie 10.33. Wydrukuj kod narzędzia sortowania topologicznego 36 i wyjaśnij koledze lub koleżance działanie funkcji tso rt. Ćwiczenie 10.34. Powtórz poprzednie ćwiczenie po uprzednim narysowaniu odpo­ wiednich schematów. Ćwiczenie 10.35. Zmierz przedziały czasu upływającego między kolejnymi przerwami w Twoim środowisku pracy. Oblicz godziny przepływu i porównaj je z całkowitym czasem, jaki spędzasz w pracy.

Dostępność narzędzi oraz dalsza lektura Jak stwierdziliśmy na początku niniejszego rozdziału, wiele prezentowanych przykła­ dów opierało się na rodzinie narzędzi dostępnych w systemach uniksowych. Są one również dostępne w nowszych wersjach systemu MacOS. Użytkownicy systemu Micro­ soft Windows również mogą z nich korzystać — podjęto wiele działań zmierzających do zapewnienia dostępu do narzędzi uniksowych w środowisku Windows. Program UWIN [Kor97], stanowiący przeniesienie narzędzi i bibliotek, obsługuje nagłówki, interfejsy oraz polecenia systemu X/Open Release 4. Cygwin*1 [Noe98] to całkowicie zgodna z architekturą Win32 warstwa przenośna dla aplikacji uniksowych. Obsługuje ona narzędzia rozwojowe GNU i pozwala na bezproblemowe przenoszenie wielu pro­ gramów uniksowych obsługując niemal wszystkie wywołania standardu POSIX.1/90 oraz inne funkcje charakterystyczne dla różnych wersji systemów. OpenNt [Wal97], obecnie rozprowadzane jako Interix**, to pełne środowisko przenoszenia oraz urucha­ miania, którego można używać w celu migracji kodu źródłowego aplikacji opracowa­ nej w tradycyjnym systemie uniksowym bezpośrednio na platformę Windows NT. Do przeniesionego oprogramowania należą między innymi klienty XI1R5 oraz ponad 200 innych narzędzi. Poza tym firma Microsoft oferuje pakiet znany jako Windows Services fo r Unix*9, który zawiera większość najważniejszych narzędzi uniksowych, natomiast firma Mortice Kem Systems 40 sprzedaje bardziej rozbudowany pakiet MKS Toolkit. Jeżeli szuka się zintegrowanego środowiska programowania niezależnego od platfor­ my, warto zapoznać się z systemem Eclipsé41. 56netbsdsrc/usr.bin/tsort. 37http://sources,redhat. com/cygwin/. 38

http://www.interix.com.

39

http://www.microsoft. com/windows/sfu.

40

http://www.mks. com/.

41 http://www.eclipse.org/.

354

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Zestaw narzędziowy outw if1 [SpiOO] uzupełnia te wysiłki poprzez udostępnienie narzę­ dzi opartych na zasadach projektowych narzędzi uniksowych i pozwala na przetwarza­ nie danych aplikacji systemu Windows za pomocą wyrafinowanych potoków danych. Narzędzia systemu outwit oferują dostęp do schowka Windows, rejestru, dziennika zdarzeń, relacyjnych baz danych, właściwości dokumentów oraz łączy powłoki. Wreszcie, jeżeli nie można znaleźć gotowych narzędzi dla własnego środowiska, można przenieść i skompilować narzędzia na podstawie bazy kodu źródłowego dostępnej na płycie dołączonej do książki. Autor z rzewnością wspomina gwałtowny wzrost wydaj­ ności działania po zaimplementowaniu podstawowej wersji narzędzia grep w przypadku pochodzącej z lat 70. maszyny Perkin-Elmer działającej pod kontrolą systemu OS/32. Najpełniejszym źródłem informacji na temat wyrażeń regularnych jest książka Friedla i Orama [FO02], Praktyczne szczegóły dotyczące implementacji narzędzi leksykalnych można znaleźć w artykułach Aho i Corasicka [AC75] oraz Hume'a [Hum 88]. Teore­ tyczne podstawy wyszukiwania ciągów znaków w oparciu o wyrażenia regularne można znaleźć w pozycjach Aho i in. [AHU74, ASU85], Oprócz programu grep i edytora, innymi narzędziami przydatnymi w przypadku przetwarzania tekstu są sed [McM79, DR97b], awk [AKW88, DR97a], powłoki systemów uniksowych [Bou79, KP84, Bli95, NR98] oraz języki Perl [WCSP00, SCW01] i Python [Lut02]. Perl to jeden z najbar­ dziej wszechstronnych języków w zakresie tworzenia niewielkich narzędzi. Pozycja Schwartza i in. [SCW01] to standardowy tekst wprowadzający. Wiele prac [WCSP00. Sri97, CT98] jest skierowanych do zaawansowanych użytkowników. Tworząc własne narzędzia pomocne w czytaniu kodu, można korzystać z technik używanych w celu szybkiego tworzenia języków dziedzinowych [SpiO 1]. Metodę i narzędzia przeglądania podpisów opisano w pracy Cunninghama [CunOl], a jeśli Czytelnik jest zainteresowany technikami wizualizacji, pozycja Tuftego [Tuf83] stanowi zarówno wartościowy pod­ ręcznik. jak i interesującą ozdobę księgozbioru. Narzędzie glimpse opisano w pozycji Manbera i Wu [MW 94]. Przystępne omówienie wpływu stylów typograficznych na czytelność kodu zawiera praca Omana i Cooka [OC90]. Oryginalne narzędzie spraw­ dzania programów lint zostało po raz pierwszy udokumentowane jako raport techniczny [Joh77], który następnie powtórzono w podręczniku programisty systemu Unix [Joh79], Często występujące problemy i kwestie przenośności związane z językiem C, o których kompilator powinien (choć nie zawsze) ostrzegać, opisano w trzech pozycjach [Koe88, Hor90, RS90]. Jeżeli Czytelnik jest zafascynowany sztuką pisania całkowicie nieczy­ telnych programów, więcej informacji na ten temat znajdzie w pozycji Libesa [Lib93], Działanie programu profilującego wykonanie gprof omówiono szczegółowo w pozy­ cjach Grahama i in. [GK.M82, GK.M83]. Projekt frontonu graficznego debuggera opi­ sano w pozycji Adamsa i Muchnicka [AM 86], Obecnie standardową notacją służącą do rysowania różnych diagramów związanych z programowaniem jest UML [FS00, BRJ99. RJB99], Programowanie ekstremalne (ang. extreme programming) [BecOO] to proces rozwojowy, który wspiera programowanie w parach. Pojęcie przepływu w kon­ tekście produktywnych środowisk pracy opisano w dwóch pozycjach [DL99, Bra86],

42 http://w w w . dmst. aueb.gr/dds/sw /outw it.

Rozdział 11.

Pełny przykład Większość przykładów przedstawionych w niniejszej książce dotyczyła wyodrębnio­ nych fragmentów kodu, który staraliśmy się zrozumieć. W niniejszym rozdziale spraw­ dzimy dzięki obszernemu przykładowi, w jaki sposób umiejętności czytania i rozumie­ nia kodu można stosować w praktyce. Naszym celem będzie usprawnienie bazy danych hsąldb, tak aby wewnętrznie obsługiwała nową funkcję SQL związaną z datą i czasem. Funkcja, którą chcemy dodać, to PHASEOFMOON, zwracająca fazę księżyca dla danej daty jako liczbę z przedziału od 0 do 100. gdzie 0 reprezentuje nów, zaś 100 — pełnię. Li­ czący ponad 34 000 wierszy kodu źródłowego system hsąldb nie stanowi przypadku banalnego. Wprowadzenie drobnych modyfikacji, takich jak usunięcie błędu lub doda­ nie rozszerzenia, w przypadku względnie obszernego kodu jest często wykonywanym zadaniem, które niemal zawsze wymaga posiadania doskonałych umiejętności w zakre­ sie czytania kodu. Ponadto zastosujemy techniki czytania kodu w celu zlokalizowania i przeniesienia istniejącej implementacji algorytmu oraz zdiagnozowania poprawności wprowadzonych modyfikacji. Poniżej zawarto chronologiczny opis całego procesu w formie osobistego dziennika (stąd też napisanego w narracji pierwszoosobowej).

11.1. Przegląd Rozpoczynam od zbadania głównego katalogu systemu w celu uzyskania informacji na temat ogólnej struktury dystrybucji systemu hsąldb. Brak pliku README, jednak plik 0 nazwie index.html' wydaje się obiecującym punktem wyjścia do dalszych poszukiwań. 1 rzeczywiście, opisuje on zawartość wszystkich podkatalogów katalogu głównego i daje ogólne wyobrażenie o całym układzie.

1 hsqldb/index.html.

356

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Moim kolejnym krokiem będzie skompilowanie systemu z kodu źródłowego i urucho­ mienie go. Daje to wiele korzyści. Po pierwsze, poznam proces budowy systemu — ze względu na fakt. że zmieniając system będę go konsolidował wielokrotnie, możli­ wość sterowania tym procesem będzie miała ogromne znaczenie. Ponadto upewnię się, że posiadany kod źródłowy rzeczywiście da się skompilować, a dzięki temu uniknę straty czasu w przypadku, gdyby okazało się, że tak nie jest. Powody niemożności skom­ pilowania systemu mogą być różnorakie: uszkodzony kod źródłowy, nieodpowiednie środowisko kompilacji, kompilator lub narzędzia, brak bibliotek lub komponentów, nieprawidłowa konfiguracja. Wszystkie należy jednak rozpoznać jak najszybciej w celu zminimalizowania ryzyka ukierunkowania swoich działań w złą stronę. Ponadto, uru­ chomienie systemu pozwoli poznać z grubsza jego funkcjonalność (muszę pamiętać, że chcę zminimalizować czas związany z czytaniem dokumentacji i kodu źródłowego) oraz pozwoli na przeprowadzenie eksperymentów z użyciem przypadków testowych, które później wykorzystam w celu zweryfikowania poprawności wprowadzonych zmian. Wreszcie, system, który kompiluje się i uruchamia, stanowi solidny punkt wyjścia do dalszych działań — wszelkie błędy kompilacji lub uruchomienia występujące od tej pory będą oznaczały błąd popełniony przeze mnie. Jednocześnie będę wiedział, że moje zmiany nie odpowiadają za żadne problemy, które mogłyby wystąpić teraz. Plik index.html zawiera odnośnik dokumentujący proces budowy systemu. Uruchomie­ nie skryptu build.bal2 powoduje rozpoczęcie budowy. W celu zweryfikowania tego faktu muszę uruchomić system. Kolejny odnośnik z sekcji dokumentacji na stronie index.html kieruje mnie do kopii witryny WWW systemu hsąldb. Dzięki selektywne­ mu przejrzeniu jej zawartości zaczynam rozumieć sposób funkcjonowania pakietu oraz jego podstawowe tryby działania. W katalogu bin uruchamiam menedżera bazy danych (run DatabaseManager) i rozpoczynam swoją pierwszą interaktywną sesję. Ponieważ będę dodawać nową funkcję daty, czytam dokumentację składni języka SQL dostępną w zasobach witryny WWW i eksperymentuję z kilkoma odpowiednimi poleceniami. create table test (d date): insert Into test values('2002-09-22'): select year(d) from test:

Ostatnie polecenie wyświetla jeden rekord z pojedynczą kolumną, zawierającą, zgodnie z oczekiwaniami, wartość 2002 .

11.2. Plan działania Teraz muszę opracować plan dalszych działań. Najpierw poszukam w dokumentacji języka SQL funkcji z atrybutami i typami podobnymi do tych, które ma obsługiwać dodawana funkcja. Następnie zaprojektuję ją zgodnie ze strukturą istniejących funkcji. Aby móc efektywnie wykorzystać mechanizm dopasowywania do wzorca w trakcie przeszukiwania kodu, wybiorę funkcję o rzadko używanej nazwie (DAYOFWEEK) zamiast takich, jak YEAR lub HOUR. 2

hsqldb/srdbuild. bal.

3 hsqldb/doc/intemet/hSql. html.

Rozdział 11. ♦ Pełny przykład

357

Teraz mogę poszukać wystąpień tej funkcji, aby uzyskać ogólne wyobrażenie o tym, które pliki będzie trzeba zmodyfikować. $ cd src/org/hsqldb $ grep -1 dayofweek *.java Library.java: [...]. "DAYOFWEEK". Library.java: "org.hsqldb.Library.dayofweek". "DAYOFYEAR". Library.java: public static int dayofweekljava.sql Date d) {

Otwieram plik Library.java i szukam ciągu DAYOFWEEK. Pierwsze wystąpienie napotykam w następującym kontekście4. final static String sT1meDate[] - ( “CURDATE”. "org.hsqldb.Library.curdate”. "CURTIME",

[...] "DAYOFMONTH". "org.hsqldb.Library.dayofmonth". "OAYOFWEEK". "org.hsqldb.Library.dayofweek". "DAYOFYEAR".

Bez wątpienia tablica sTimeDate odwzorowuje funkcje SQL na odpowiednie implemen­ tacje w języku Java. Powtarzam moje wyszukiwanie bez uwzględniania wielkości liter, w celu zlokalizowa­ nia wystąpień funkcji dayofweek. Drugie (i ostatnie) wystąpienie tego ciągu to definicja metody statycznej5. /** * Method declaration * @param d * Oreturn

*/ public static 1nt dayofweekljava.sql Date d) { return getDatellmeParttd. Calendar.DAY_OF_WEEK):

1

Jak widać, metoda nie została opatrzona żadnym istotnym komentarzem, jednak sądzę, że muszę zrozumieć jej działanie, gdyż mój nowy kod będzie mial podobną postać. Zakładając, że funkcja getDateTimePart jest częścią biblioteki języka Java, otwieram dokumentację JDK i zapoznaję się ze szczegółami klasy Calendar. Jednak nie znajduję żadnych odnośników do metody getDateTimePart, więc kontynuuję moje poszukiwania w kodzie źródłowym systemu hsąldb. Okazuje się, że jest ona używana w wielu róż­ nych przypadkach, udaje mi się też odszukać jej definicję6. private static 1nt getDateTlmePart(java.ut11 Date d. int part) { Calendar c - new Gregor1anCalendar(): c.setTime(d); return c.get(part):

1

Ponownie, definicja jest poprzedzona niewypełnionym szablonem komentarza javadoc — nie jest on zatem zbyt pomocny. Widać jednak, że funkcja tworzy c jako instancję klasy Calendar, ustawia jej wartość na datę przekazaną jako argument i zwraca element klasy Calendar określony przez parametr part. 4 hsqldb/src/org/hsqldb/Library.java\ 85 - 89. 5 hsqldb/src/org/hsqldb/Library.java\ 8 1 4 - 8 2 4 . 6 hsqldb/src/org/l)sqldb/Librar)'.java: 777 - 783.

358

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Moje zadanie polega na zapewnieniu zwrócenia części związanej z fazą księżyca. Roz­ poczynam od wyszukania słowa „moon” w dokumentacji klas języka Java Calendar oraz GregorianCalendar. Nie znajduję niczego przydatnego, więc kontynuuję czytanie uwag zawartych w dokumentacji JDK, dotyczących architektury klasy GregorianCa­ lendar. Szczegóły dotyczące jej implementacji są fascynujące, jednak uświadamiam sobie, że w celu określenia fazy księżyca będę musiał napisać nowy kod.

11.3. W ielokrotne użycie kodu Nie chcąc ponownie odkrywać kola, decyduję się na wyszukanie ¡śmiejącej implemen­ tacji algorytmu obliczania faz księżyca. Mógłbym przeszukać zasoby sieci WWW lub repozytorium oprogramowania open-source przy użyciu wyszukiwarki, jednak zaczy­ nam od przeszukania zawartości płyty CD-ROM dołączonej do książki. Szukając słowa „moon” szybko uświadamiam sobie, że będę musiał oddzielić informacje istotne od nieistotnych. $ find -type -f -print | xargs grep -i moon ./netbsdsrc/games/batt1esta r/ni ghtf11e .c : "Feather palms outlined by mellow moonlight and a silvery black ocean Hne\n\ ./netbsdsrc/games/batt1esta r/n1ghtf11e .c : by a huge tropical moon stretches at least 30 meters inland.\n\

[...] ./netbsdsrc/games/hack/hack.main.c: plineC'You are lucky! Full moon tonight.“); /netbsdsrc/games/pom/pom.c: * Phase of the Moon. Calculates the current phase of the noon.

[...] ./netbsdsrc/lib/Hbc/stdlib/malloc.c: * a memory fault 1f the old area was tiny, and the moon

Wśród wielu fikcyjnych odwołań i satyrycznych komentarzy, kod w pliku pom.c wydaje się faktycznie obliczać fazę księżyca. Szybkie spojrzenie na stronę podręcznika systemu NetBSD potwierdza to, jak również udostępnia pewne dodatkowe humorystyczne in­ formacje. Narzędzie pom wyświetla bieżącą fazę księżyca. Jest przydatne w przypadku wybierania docelowych dat zakończenia prac programistycznych oraz przewidywania zachowania kierownictwa. Kontynuuję lekturę kodu źródłowego programu w celu wyodrębnienia algorytmu ob­ liczania faz księżyca i przeniesienia go do kodu źródłowego Java systemu hsqldb. Komentarz zawiera informacje o książce, w której można znaleźć szczegóły dotyczące algorytmu8. * Based on routines from 'Practical Astronomy with Your Calculator'. * by Duffett-Smlth. Comments give the section from the book that [...]

Jest to potencjalnie przydatna informacja, jednak najpierw spróbuję zrozumieć działanie algorytmu na podstawie samego kodu źródłowego. 7 netbsdsrc/gam es/pom /pom .c. g

netbsdsrc/games/pom/pom.c\ 54 - 55.

Rozdział 11. ♦ Pełny przykład

359

Najobszerniejszą funkcją w pliku jest funkcja potm, która wydaje się obiecującym punk­ tem wyjścia . double potm(days) .double days:

Ponownie, komentarz dołączony do funkcji (potm —return phase of the moon) nie jest zbyt przydatny dla celów innych niż uzasadnienie nadania jej takiej, a nie innej nazwy. Przyjmując, że nie mam żadnych innych przesłanek co do znaczenia argumentu days, lokalizuję wywołanie funkcji w celu uzyskania podpowiedzi na podstawie spo­ sobu jej użycia"'. today - potm(days) + .5:

Następnie przeglądam wcześniejsze partie kodu w celu sprawdzenia, w jaki sposób jest otrzymywana wartość zmiennej days". struct timeval tp: struct timezone tzp; struct tm *GMT; t1me_t tmpt; double days, today, tomorrow: 1nt cnt: If (gettimeofday(&tp,&tzp)) errd. "gettimeofday"): tmpt - tp.tv_sec: SIT - gmtime(&tmpt); days * (GMT->tm.yday + 1) + ((GMT->tm hour + (GMT->tm_min / 60.0) + (GMT->tm_sec / 3600.0)) / 24.0): for (cnt - EPOCH: cnt < GMT->tm.year: ++cnt) days +- isleap(cnt) ? 366 : 365:

Kod, oprócz stałej EPOCH, używa głównie standardowych funkcji ANSI-C i POS1X. Na tej podstawie stwierdzam, że mogę zrekonstruować w języku Java argument potm, który będzie działał w ten sam sposób bez konieczności dalszego analizowania faktycz­ nego znaczenia argumentu days. Zauważam, że kod wydaje się obliczać liczbę dni od EPOCH do tp. Nie jestem pewien, w jaki sposób i dlaczego jest używana funkcja isleap, jednak decyduję się zająć tym problemem później. Kontynuuję przeszukiwanie kodu źródłowego wstecz, szukając stałej EPOCH. Pozwala to na odkrycie wielu stałych, które prawdopodobnie również będą musiały zostać prze­ niesione do kodu Java12. ♦define #deflne ♦define ♦define ♦define ♦define ♦define ♦define o

PI EPOCH EPSILONg RHOg ECCEN lzero Pzero Nzero

3.141592654 85 279.611371 282.680403 0.01671542 18.251907 192.917585 55.204723

netbsdsrc/games/pom/pom.c: 133- 135.

10netbsdsrc/games/pom/pom. c: 104. 11 netbsdsrc/games/pom/pom.c: 89 - 103. 12 netbsdsrc/games/pom/pom.c: 70 - 77.

/* solar /* solar /* solar /* lunar /* lunar /* lunar

ecliptic long at EPOCH */ ecliptic long of perigee at EPOCH */ orbit eccentricity */ mean long at EPOCH */ mean long of perigee at EPOCH */ mean long of node at EPOCH */

360

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Używam w moim edytorze (w) następującej operacji zastępującej tekst, wykorzystu­ jącej wyrażenie regularne 13 :’a.’bs/#defineAI\([AAI ]*\)[ Al]*\( *\>AI\(,*\>/AI\3AMAI private static final double U l - U2:

w celu automatycznego przekształcenia większości przedstawionych powyżej definicji preprocesora na definicje stałych języka Java. private static final double PI - 3.141592654. private static final double EPOCH - 85: /* solar ecliptic long at EPOCH */ private static final double EPSILONg - 279.611371: /* solar ecliptic long of perigee at EPOCH */ private static final double RHOg - 282.680403; /* solar orbit eccentricity */ private static final double ECCEN - 0.01671542: /* lunar mean long at EPOCH */ private static final double lzero * 18.251907: /* lunar mean long of perigee at EPOCH */ private static final double Pzero - 192.917585: I* lunar mean long of node at EPOCH */ private static final double Nzero - 55.204723:

W celu sprawdzenia, czy isleap jest standardową funkcją języka C, szybko kompiluję prosty program testowy. #1nclude mainO { int i - isleap (444); )

Funkcja wydaje się niezdefiniowana, więc zapewne jest niestandardowa. Dlatego szukam jej definicji w odpowiednich plikach14. ♦define isleap(y) ((((y) * 4) — 0 && ((y) * 100) !- 0) || ((y) * 400) — 0)

Na podstawie jej definicji stwierdzam, że funkcja pobiera jako argument rok i zwraca wartość prawda, jeżeli rok jest przestępny. Szukam słowa „leap” w klasie języka Java GregorianCalendar — odkrywam, że funkcja jest obsługiwana w ten sam sposób co metoda isleapYear. Kontynuuję przeglądanie plikupom.c, szukając innych funkcji, których przeniesienie do implementacji w języku Java może okazać się konieczne. Dla każdej z dwóch znalezio­ nych funkcji (dtor oraz adj360) przeszukuję kod źródłowy w celu sprawdzenia, gdzie jest używana. Obie są wykorzystywane w funkcji potm, więc ich przeniesienie jest konieczne. 13 Chociaż wyrażenie to może wyglądać na pozbawiony sensu ciąg znaków, jego analiza nie nastręcza zbyt

dużych problemów. Wszystko, co wykonuje polecenie to (tu należy wziąć głęboki wdech): od punktu w kodzie oznaczonego jako a do punktu oznaczonego jako b ('a, 'b) zastąp (s/) ciąg #def ine, po którym występuje znak tabulatora (AI), po którym występuje przechowywany ( \ ( . . . \ )) ciąg \ 1 : dowolny znak oprócz tabulatora i spacji ([“ I ]) powtórzony zero lub większą liczbę razy (*), po którym występuje spacja lub tabulatur ([ *1 ]), po którym występuje przechowywany ciąg \ 2, który może zawierać dowolny znak powtórzony zero lub większą liczbę razy ( .*), po którym występuje tabulator CI), po którym występuje przechowywany ciąg \3, który również może zawierać dowolny znak powtórzony zero lub większą liczbę razy następującą wartością (/): tabulatorem, po którym występuje przechowywany ciąg \3 (komentarz), po którym występuje znak nowego wiersza (AM), po którym występuje tabulator oraz ciąg private static final double, po którym występuje przechowywany ciąg \1 (nazwa stałej), po którym występuje znak -, po którym występuje przechowywany ciąg \2 (wartość stałej). 14 netbsdsrc/include/tzfile.h: 151.

Rozdział 11. ♦ Pełny przykład

361

Teraz skupiam swoją uwagę na próbie zrozumienia, co reprezentuje wartość zwracana przez funkcję potm. Odpowiedni kod jest całkowicie niezrozumiały15. return(50 * (1 - cos(dtortD)))):

/* sec 63 #3 */

Jednak sekwencja kodu w miejscu wywołania funkcji sugeruje, że faza jest zwracana jako liczba zmiennoprzecinkowa z zakresu od 0 do 100 , która reprezentuje fazę księ­ życa od nowiu do pełni16. 1f (Ont)today ~ 100) (void)printf("Full\n“); else 1f (Ulnt)today) (vo1d)printf(“New\n“):

Dalsze przypadki użycia tej wartości potwierdzają to przypuszczenie, gdyż bez wątpienia 50 reprezentuje dwie kwadry17. 1f (Ont)today — 50) (void)pnntf(“is\n\ tomorrow > today ? "at the First Quarter" : “at the Last Quarter“):

Oryginalny kod zawarty w pliku pom.c'x wyświetla bieżącą fazę księżyca. Ze względu na fakt, że metoda moonPhase będzie obliczać fazę księżyca dla daty przekazanej jako jej argument, uznaję, że kod języka C manipulujący bieżącą datą nie powinien być bezpo­ średnio przenoszony do kodu Java. Muszę poświęcić nieco więcej czasu na zrozumienie znaczenia zmiennej days. Pierwszy etap obliczeń19: days - (GMT->tm_yday + 1) + ((GMT->tm_hour + (GMT->tm_min / 60.0) + (GMT->tm_sec / 3600.0)) / 24.0): for (cnt - EPOCH: cnt < GMT->tm_year: ++cnt) days +- 1sleap(cnt) ? 366 : 365; today - potm(days) + .5:

zawiera wyrażenie dzielone przez 24 oraz dodawanie stałej 365. Wskazuje to, że zmienna days prawdopodobnie zawiera liczbę dni (w tym część ułamkową), jakie upłynęły od EPOCH. Jeżeli moje rozumowanie jest poprawne, wówczas kod można przepisać w języ­ ku Java w kontekście różnicy wartości getTime między obiektem Date przekazanym jako argument metody moonPhase a obiektem Date reprezentującym EPOCH. GregorlanCalendar e new GregorianCalendartEPOCH. Calendar.JANUARY. 1): return potm((d.getTime() - e.getT1me().getT1meO) / 1000.0 / 60.0 / 60.0 / 24.0);

Jednak wiele kwestii pozostaje otwartych i nie da się ich w prosty sposób rozstrzygnąć na podstawie samego kodu: kwestia stref czasu oraz spójnej obsługi lat przestępnych w przypadku implementacji w języku C a Java. W tym celu decyduję, że zamiast po­ święcać więcej czasu na próby zrozumienia kodu, przetestuję swoją implementację w języku Java względem wyników otrzymanych za pomocą wersji w języku C. 15

netbsdsrc/games/pom/pom. c:

163.

16netbsdsrc/games/pom/pom.c: 106- 109.

^netbsdsrc/games/pom/pom.c: 112-114. 18

19

netbsdsrc/games/pom/pom. c. netbsdsrc/games/pom/pom.c\ 100- 104.

362

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Resztę kodu przenoszę niemal bez zmian, nie próbując go zrozumieć. Funkcja adj360 wymaga nieco większego nakładu pracy, gdyż pobiera jako argument wskaźnik. Czytając kod zauważam, że wskaźnik jest używany do przekazania argumentu przez referencję i zmodyfikowania jego wartości w kontekście wywołania20. void adj360(double *deg)

{

for (::) if (*deg < 0) *deg +" 360: else if (*deg > 360) *deg -- 360: else break:

Mogę w banalny sposób zmodyfikować funkcję i jej wywołanie poprzez dodanie na jej końcu instrukcji return. Jest to dla niej jedyny punkt wyjścia. Równoważny kod w języku Java będzie miał zatem postać: private static double adj360(double deg)

(

for (:;) if (deg < 0) deg +* 360: else if (deg > 360) deg -• 360: else break; return (deg):

) Wywołania funkcji sin oraz cos muszą zostać poprzedzone przedrostkiem Hath; podob­ nie, wykorzystuję możliwość użycia oferowanej przez język Java definicji Math. PI w celu zastąpienia definicji z programu w języku C. Na listingu 11.1 przedstawiono pierwszą wstępną wersję algorytmu obliczania faz księżyca przepisanego do kodu w języku Java. Listing 1 1 .1 . A lg o rytm o b licza n ia f a z księżyca p rz e p isa n y

M’k o d zie ję z y k a J a v a

import Java .util.*: class MoonPhase { private static final int EPOCH - 85: /* solar ecliptic long at EPOCH */ private static final double EPSILONg - 279.611371: [ . . . ] • ------------------------------------------------------------------------------------------------------------------------- Inne slate

public static double moonPhase(Date d) ( • — Funkcja GregorianCalendar e - new GregonanCalendariEPOCH, Calendar.JANUARY. 1): sierujqca return potm((d.getT1me() - e.getHmeO.getTlmeO) 7 1000.0 / 60.0 / 60.0 / 24.0):

} public static void main(String args[]) { • - Test GregorianCalendar t - new GregorianCalendar(2002. Calendar,SEPTEMBER. 30): System.out.println(moonPhase(t.getTimet))):

} 20 netbsdsrc/games/pom/pom.c: 181 —192.

Rozdział 11. ♦ Pełny przykład

363

private static double potm(double days) {

[

]

Ec - 360 / Math.PI * ECCEN * Math.Sin(dtor(MSOl)): • — Utycie klasy Math języka Java [ . . . ] • -----------------------------------------------------------------------------Niemal wierna kopia oryginalnego kodu w języku C

private static double dtoridouble deg) { return(deg * Math.PI / 180):

) private static double adj360(double deg) { [...]

• -----------------------------------------------------------------------------Patrz lekst

)

11.4. Testowanie i uruchamianie Swoją implementację klasy MoonPhase uzupełniam prostą procedurą testującą w celu zweryfikowania poprawności jej działania. public static void malnfStrlng args[]) { GregorlanCalendar t - new GregorianCalendar(2002. Calendar.SEPTEMBER. 30); System.out.printl n(moonPhase( t .getUmet))):

) Przetestowanie klasy przy użyciu kilku dni dla pełni (które mogę określić na podstawie kalendarza w swoim organizerze) pozwala stwierdzić, że moja wstępna implementacja nie działa poprawnie. Fragment, który najprawdopodobniej jest błędny, to kod, który całkowicie zaimplementowałem ponownie w języku Java21. for (cnt - EPOCH: cnt < GMT->tm_year; ++cnt) days +- Isleap(cnt) ? 366 : 365:

W powyższym fragmencie stała EPOCH jest porównywana z tmj/ear, więc można przyjąć uzasadnione założenie, że oba elementy odwołują się do tych samych wielkości. Szyb­ kie przejrzenie strony podręcznikowej dla funkcji ctime pozwala znaleźć następującą definicję tm_year. 1nt tm_year;

/* year - 1900 */

Zatem tm_jear posiada niejawne przesunięcie o 1900. Analogiczna dokumentacja klas Java Calendar oraz GregorianCalendar nie zawiera informacji o użyciu takiego prze­ sunięcia. Stąd, muszę odpowiednio dostosować wartość EPOCH. private static final Int EPOCH - 1985:

Po wprowadzeniu tej zmiany, kod w języku C i Java wydaje się działać tak samo, mogę więc przejść do modyfikowania kodu systemu hsqldb. Zgodnie z moim rozumieniem jego działania, jedyna zmiana jakiej muszę dokonać to dodanie nowego wpisu w tablicy sTimeDate. "org.hsqldb.MonnPhase.moonPhase”. "PHASEOFMOON".

21 netbsdsrc/games/pom/pom. c: 102- 103.

Czytanie kodu. Punkt widzenia twórców oprogramowania opert-source

364

Po skonsolidowaniu kodu źródłowego jestem gotów do przetestowania nowej funkcji. Przygotowuję niewielki skrypt testowy w języku SQL, którego będę używał w celu sprawdzania działania programu. create table test (d date); insert into test values('2002-09-22') select phaseofmoon(d) from test:

Uruchomienie powyższego skryptu powoduje wyświetlenie komunikatu o błędzie: „une­ xpected token: PHASEOFMOON”.Najwidoczniej muszę nieco bliżej przyjrzeć się metodzie

rozszerzania funkcjonalności systemu hsqldb. Rozpoczynam od zbadania skompilowa­ nych plików klas w celu upewnienia się, że mój kod faktycznie został skompilowany. Znajduję je w katalogu classes/org/hsąldb, jednak katalog nie zawiera żadnego śladu przekompilowania klasy Library lub skompilowanej klasy MoonPhase. Ponownie czyta­ jąc skrypt budowy, którego używam, odkrywam, że skompilowane pliki są bezpośred­ nio składowane w pliku ja r (Java archive). Dlatego też badam plik ja r, szukając klasy MoonPhase. $ jar tvf hsqldb.jar | grep -1 moon 2061 Mon Sep 30 11:30:32 GMT+03:00 2002 org/hsqldb/MoonPhase.class

Skompilowany plik istnieje, więc powód nierozpoznawania znacznika leży gdzie indziej. Ponawiam wyszukiwanie wystąpień ciągu znaków dayofweek w celu sprawdzenia, czy czegoś nie pominąłem — bezskutecznie. Jednakże ponownie badając kod. który dodałem "org.hsqldb.MonnPhase.moonPhase". "PHASEOFMOON”.

zauważam, że popełniłem błąd w zapisie, który poprawiam ”org.hsqldb.MoonPhase.moonPhase". "PHASEOFMOON".

Mimo to, po ponownym skonsolidowaniu systemu hsqldb wciąż otrzymuję błąd „une­ xpected token: PHASEOFMOON”.

Teraz szukam kodu, który jest odpowiedzialny za wyświetlenie komunikatu o błędzie. $ grep "unexpected token" *.java (no results) I grep unexpected *.java (no results)

Stwierdzam, że prawdopodobnie pominąłem jakiś fragment kodu źródłowego: kodu, który zawiera komunikat o błędzie „unexpected token” oraz kodu, który powinienem był zmodyfikować. Rekurencyjne przeszukanie katalogu dla innych plików źródłowych Java nie pozwala odkryć niczego interesującego. Próbuję wyszukiwania niezależnego od wielkości liter — komunikat mógł być sformatowany w dziwny sposób. Tym razem otrzymuję wiele wyników. $ grep -1 unexpected *.java Access.java: throw Trace.error(Trace.UNEXPECTED_TOKEN. right): Database.java: throw Trace.error(Trace.UNEXPECTED TOKEN. sToken):

[...] Function.java: Trace.checkd !- -1. Trace.UNEXPECTED_TOKEN. function): Parser.java: throw Trace.error(Trace.UNEXPECTE0_T0KEN. token):

[...] Tokenlzer.java: throw Trace.error(Trace.UNEXPECTED_TOKEN. sToken):

[...]

Rozdział U L ♦ Pełny przykład

365

Trace.java: UNEXPECTEDJOKEN - 10.

[...I Trace.java: "37000 Unexpected token". "37000 Unexpected end of command".

Okazuje się, że poszukiwałem komunikatu o błędzie błędnie zapisanego. W aplikacji rozpoczyna się on od wielkiej litery U. Jako efekt uboczny popełnionego błędu znajduję odwołania do stałej Trace. UNEXPECTEDJOKEN w plikach Parser.java oraz Tokenizer.java. Na podstawie ich nazw stwierdzam, że Parser.java obsługuje gramatykę języka SQL, zaś klasa Tokenizer obsługuje analizę leksykalną. Uznaję, że błąd jest najprawdopo­ dobniej związany z tą ostatnią. Szybko przeglądam kod pliku Tokenizer.java w celu zidentyfikowania problemu. Znaj­ duję sekwencję kodu inicjalizacji tabeli słowami kluczowymi. Edytor, którego używam, oznacza odrębnym kolorem ciągi znaków, więc cały blok od razu się wyróżnia". String keyword!] - { "ANO". "ALL". "AVG”. "BY". "BETWEEN". "COUNT”. "CASEWHEN". "DISTINCT". “DISTINCT". "EXISTS". "EXCEPT". "FALSE". "FROM". "GROUP”. "IF". "INTO". "IFNULL". "IS". “IN”. "INTERSECT". "INNER". "LEFT". “LIKE". "MAX". "MIN". "NULL". "NOT". "ON", "ORDER". "OR". “OUTER". "PRIMARY". "SELECT". "SET". "SUM". "TO". "TRUE". "UNIQUE". "UNION". "VALUES". "WHERE". "CONVERT". "CAST”. "CONCAT". "MINUS". "CALL"

1:

Tuż poniżej widzę, że ciągi znaków są wstawiane do innej struktury23. for (1nt 1 - 0: i < keyword.length; i++) { hKeyword.put(keyword[1], hKeyword):

1

Następnie wyszukuję ciągu hKeyword w celu sprawdzenia, co jeszcze zostaje zawarte w tej strukturze, gdyż na razie nie ma w niej nazw większości funkcji SQL. Widzę, że hKeyword to instancja klasy Hashtable, lokalna dla klasy i używana w celu identyfiko­ wania słów kluczowych w metodzie wasName. Żadna inna metoda nie dodaje elementów do zmiennej hKeyword, a ponieważ jest ona lokalna dla klasy, nie ma potrzeby prowa­ dzenia dalszych badań. Jednakże jedno z użyć zmiennej hKeyword pozwala znaleźć ko­ lejny ślad24. return !hKeyword.contai nsKeytsToken);

Sprawdzam, w jaki sposób jest używana zmienna sToken, jednak okazuje się, że stanowi główny element skomplikowanego analizatora leksykalnego, więc zarzucam ten pomysł. Po osiągnięciu końca tej drogi, ponownie skupiam uwagę na kodzie źródłowym Library, java i badam użycie tablicy, do której wstawiałem nową funkcję. Stwierdzam, że jest ona lokalna dla klasy25. final static String sTimeDate[] - ( 22

hsqldb/src/org/hsqldb/Tokenizer.java: 66 - 75.

23 hsqldb/src/org/hsqldb/Tokenizer.java\ 76 - 78. 24hsqldb/src/org/hsqldb/Tokenizer.java: 192. 25 hsqldb/src/org/hsqldb/Library.java: 85.

366

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Zatem muszę zbadać tylko ten plik. Zwracam uwagę na asymetrię występującą między dwoma ostatnimi wierszami. "YEAR", "org.hsqldb.Library.year". "org.hsqldb.MoonPhase.moonPhase". "PHASEOFMOON”,

Bez wątpienia wstawiłem swoją definicję w niepoprawnej kolejności. Wcześniejsze wiersze miały inny format26. "org.hsqldb.Library.dayofweek". "DAYOFYEAR". “org.hsqldb. Library.dayofyear". "HOUR", "org.hsqldb.Library.hour”.

Nie zwróciłem bliższej uwagi na dokładny zapis stałych — to stąd wziął się mój błąd. Gdybym był odpowiedzialny za kod źródłowy, przeformatowałbym kod inicjalizacji: "CURDATE", "org.hsqldb.Library.curdate". “CURTIHE". "org.hsqldb.Library.curtlme". "DAYNAME". "org.hsqldb.Library dayname". "DAYOFMONTH”. "org.hsqldb.L1brary.dayofmonth". "DAYOFWEEK". "org.hsqldb.Library.dayofweek". "DAYOFYEAR". "org.hsqldb.Library.dayofyear". "HOUR". "org.hsqldb.Library.hour".

[...] do postaci dwukolumnowej: "CURDATE", "CURTIHE". "DAYNAME". ’OAYOFMONTH“. "DAYOFWEEK". "DAYOFYEAR". "HOUR".

"org.hsqldb.LIbra ry.curdate". "org.hsqldb.Library.curtlme". "org.hsqldb.Library.dayname". "org.hsqldb.Library.dayofmonth". "org.hsqldb.L1brary.dayofweek". "org.hsqldb.Li brary.dayofyear". "org.hsqldb.Library.hour".

[...] w celu uniknięcia tego rodzaju problemów w przyszłości. Poprawiam błąd i dokonuję rekompilacji. Tym razem otrzymuję nie błąd, lecz wyjątek: "java.lang.NoClassDefFoundError:

org/hsqldb/MoonPhase

(wrong name: MoonPhase)."

Wnioskuję, że problem wynika z niepoprawnej specyfikacji pakietu. Badam zatem sposób, w jaki określono strukturę pliku Library.java jako pakietu27: package org.hsqldb:

i dodaję tę samą specyfikację do klasy MoonPhase. Ponowny test przy użyciu skryptu testowego powoduje nowy błąd. Wrong data type: java.util.Date in statement [select phaseofmoon(d) from test:]

Ponownie decyduję się rozpocząć przeszukiwanie kodu według schematu wstępującego od źródła błędu. Wyszukanie komunikatu o błędzie (tym razem uwzględniając popraw­ ne użycie wielkich liter) pozwala na jego szybkie znalezienie28. S grep "Wrong data type" *.java Trace.java: [...]. "37000 Wrong data type".

26 hsqldb/src/org/hsqldb/Library.java: 89 - 90. 27 hsqldb/src/org/hsqldb/Library.java\ 36. 28 hsqldb/src/org/hsqldb/Trace.java: 108.

Rozdział 11. ♦ Pełny przykład

367

Kilka wierszy powyżej są deklarowane odpowiednie stałe. Wyszukanie słowa WRONG pozwala zlokalizować interesującą mnie zmienną29. WRONG_DATA_TYPE - 15:

Badając w kodzie pliku Trace.java relacje między stałymi błędów a komunikatami, ku swojemu niezadowoleniu stwierdzam, że stałe określające błędy oraz odpowiadające im komunikaty o błędach są przechowywane w dwóch różnych tablicach oraz że nie istnieje żaden automatyczny mechanizm (ani nawet komentarz) pozwalający na zacho­ wanie ich synchronizacji30. final static Int DATABASE ALREADY IN USE - 0. C0NNECTI0N_IS_CL0SED - 1. CONNECTIONJS_BR0KEN - 2. DATABASEJS SHUTDOWN = 3. COLUMN COUNT_OOES_NOT MATCH - 4.

[...] private static String sDescrlptlont] - { "08001 The database 1s already 1n use by another process". "08003 Connection Is closed", "08003 Connection Is broken". "08003 The database 1s shutdown", "21S01 Column count does not match”. ”22012 Division by zero”.

Zwiększam swoją czujność. Być może zmiany, które wprowadziłem, również są związane z podobnymi ukrytymi wzajemnymi zależnościami. Szukam wystąpień stałej WR0NG_ DATA_TYPE w kodzie. S grep WRONG_DATA *.java Column.java: Trace.checkd ¡-null. Trace.WRONG_DATA_TYPE. type). Column.java: throw Trace.error(Trace.WRONG_DATA_TYPE. type): Expression.java: throw Trace.error(Trace.WRONG_DATA_TYPE): jdbcResultSet.java: throw Trace.error(Trace.WR0NG_0ATA_TYPE. s): Log.java. Trace.check(check. Trace.WR0NG_DATA8ASE_FILE_VERSI0N): Table.Java: Trace.check(type — Column INTEGER. Trace.WRONG DATA TYPE, name): Trace.java: WRONG DATA_TYPE - 15. Trace.java: WR0NG_DATABASE_FILE_VERSION - 29.

Pobieżnie spoglądam na każde wystąpienie — wszystkie wydają się dość skompliko­ wane i nie udaje mi się zrozumieć, co każde z nich próbuje osiągnąć. Dlatego decyduję się wypróbować alternatywne podejście zstępujące: zamiast skupiać się na błędzie, sprawdzę sposób wywołania sw'ojej funkcji. Wyszukanie ciągu sTimeDate w pliku Library.java pokazuje, że jest on „rejestrowany” w klasie tabeli mieszającej za pomo­ cą metody publicznej31. static void reglsterth. reg1ster(h. reglster(h. reglsterih.

reg1ster(Hashtable h) { sNumerlc): sStrlng): sTimeDate): sSystem):

)

29

hsqldb/src/org/hsqldb/T race.java. 76.

30 hsqldb/src/org/hsqldb/Tra c e .ja va : 63 - 103. 31 lisqldb/src/org/hsqldb/Library.java: 108 - 113.

368

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Metoda ta jest z kolei wywoływana w pliku Database.java32. Library.register(hAli as);

Nazwa pola obiektu Hashtable (hAl i as) sugeruje, że nazwy SQL są jedynie aliasami dla odpowiednich funkcji języka Java oraz że nie są z nimi powiązane żadne inne informacje. Dlatego ponownie badam deklarację funkcji, którą staram się emulować i zauważam, że jej argument jest typu java.sql .Date33: public static int dayofweektjava.sql.Date d) {

natomiast argument nowej funkcji był typu Date. Odpowiednio zmieniam implementację metody moonPhase. public static double moonPhasetjava.sql Oate d) {

Ponadto poprawiam procedurę testującą. public static void maintString args[]) { java.sql Date d - new java.sql.Date(2002. Calendar.SEPTEMBER. 30): System.out.println(d): System.out.pri ntlntmoonPhase td)):

1

Tym razem funkcja wydaje się działać poprawnie, więc sprawdzam ją dla dodatkowych danych. create insert insert insert insert insert insert insert select

table test (d date); Into test valuest'1999-1-31'>: into test values('2000-l-21'): into test valuest'2000-3-20'): into test valuest'2002-09-8'): into test valuest'2002-09-15'): into test valuest'2002-09-22'); Into test valuest'2002-10-21'): d. phaseofmoon(d) from test:

11.5. Dokumentacja Niecierpliwy Czytelnik zapyta zapewne: „Czy to już koniec?”. Niezupełnie — musimy również zaktualizować dokumentację systemową. Szukając wystąpień ciągu d a y o fw e e k w katalogu doc, używam tej samej strategii co w przypadku lokalizowania fragmentów kodu wymagających zmiany. find . -type f -print | xargs grep -li dayofweek

Wynik wskazuje, że prawdopodobnie będzie trzeba zmodyfikować dwa pliki. ./internet/hSqlSyntax.html ./src/org/hsqldb/L1brary.html

32 hsqldb/src/org/hsqldb/Database.java: 82. 33 hsqldb/src/org/hsqldb/Library.java: 823.

Rozdział U . ♦ Pełny przykład

369

Pierwszy plik zawiera posortowaną listę funkcji związanych z czasem i datą34. Date / Time
CURDATE() (returns the current date)
CURTIME() (returns the current time)

[...]
WEEK(date) (returns the week of this year (1-53)
YEAR(date) (returns the year)

W odpowiednim miejscu dodaję więc kolejny wpis.
PHAS£0FM00N(date) (returns the phase of the moon (0-100))

Lokalizacja drugiego pliku sugeruje, że w pewien sposób odzwierciedla on strukturę kodu źródłowego i być może został wygenerowany automatycznie przez narzędzie javadoc. Dlatego sprawdzam początek pliku (pliki generowane automatycznie często rozpoczynają się od komentarza określającego ten fakt) i znajduję potwierdzenie swoich przypuszczeń35.

Wreszcie, gdybym był odpowiedzialny za konserwację kodu w jego oryginalnej dys­ trybucji, dodałbym komentarz do pliku rejestracji zmian 36 i zaktualizował jego wersję. Gdyby jednak zmiana została dokonana w projekcie wewnętrznym dla danej organiza­ cji, dodałbym w głównym katalogu źródeł plik README zawierający opis dokonanych modyfikacji.

11.6. Uwagi Typowe czynności związane z rozwojem oprogramowania wymagają zrozumienia kodu z wielu różnych powodów. W niniejszym rozdziale czytaliśmy kod w celu zidentyfi­ kowania kodu źródłowego Java oraz dokumentacji, które wymagały wprowadzenia zmian. Ponadto wyszukaliśmy i przenieśliśmy kod, który chcieliśmy wykorzystać po­ nownie, oraz podjęliśmy próbę zrozumienia i poprawienia błędów w nowoutworzonym kodzie. Należy zwrócić uwagę, w jaki sposób w kontekście rozwoju oprogramowania czytanie kodu stanowi oportunistyczne, ukierunkowane na określony cel działanie. W większości przypadków nie można sobie po prostu pozwolić na czytanie i analizo­ wanie kodu całego systemu programistycznego. Wszelkie próby precyzyjnego prze­ analizowania kodu zwykle nakazują zajęcie się wieloma klasami, plikami i modułami, co szybko zaczyna nas przerastać. Dlatego też należy starać się aktywnie ograniczać do absolutnego wymaganego minimum zakres kodu, jaki musimy zrozumieć. Zamiast starać się zyskać globalne i pełne zrozumienie kodu, powinniśmy próbować znaleźć heurystyczne skróty (niniejsza książka jest pełna tego rodzaju przykładów) i wykorzy­ stać proces budowy oraz możliwość uruchomienia systemu w celu skierowania swojej 34 hsqldb/doc/internet/hSqlSyntax.html: 477 - 492. 35 hsqldb/doc/src/org/hsqldb/Library.htmi. 5. 36 hsqldb/CHANGELOG. txt.

370

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

uwagi na te fragmenty kodu, które tego wymagają. Program grep oraz funkcja wyszu­ kiwania oferowana przez używany edytor to narzędzia podstawowe, a czasem wręcz jedyne dostępne. Pozwalają one na szybkie przeszukiwanie dużych obszarów kodu, minimalizując zakres, jaki musimy przeanalizować i zrozumieć. W wielu przypadkach możemy zajść w ślepy zaułek, napotykając kod, który bardzo trudno zrozumieć. W celu pokonania tego problemu stosujemy ogólną strategię wyszukiwania, starając się podejść do problemu czytania kodu z wielu stron do momentu, aż wreszcie uda się nam zna­ leźć odpowiednie rozwiązanie.

Dodatek A

Struktura dołączonego kodu W tabeli A.l przedstawiono strukturę kodu, który można znaleźć na płycie CD-ROM dołączonej do niniejszej książki. Poniżej zostanie przedstawiony krótki opis każdego z pakietów. Tabela A .l. Zawartość płyty CD-ROM dołączonej do książki Katalog

Oryginalny plik

Język

ace

ACE-5.2+TAO-1.2.zip

C++

apache argouml

apacheł.3.22 ArgoUML-0.9.5-src. tar.gz

c Java

cocoon demoGL

cocoon-2.0.1 -src. tar.gz demogl_src_vl3l.zip, demogl_docs_vl3l.zip

doc hsąldb

Sformatowana dokumentacja hsąldb_v. 1.61, hsąldb_devdocs.zip

Java C++ PDF

JI4 netbsdsrc

jakarta-tomcat-4.0-src

Java Java C

OpenCL perl

netBSD 1.5_ALPHA OpenCL-0.7.6. tar.gz per1-5.6.1

c

purenum

purenum.tar.gz

C++

ątchat

ątchat-0.9.7. tar.gz

C++

socket

socket+Jr-¡.Ił. tar.gz

vcf XFree86-3.3

vcf.0.3.2.tar.gz netBSD 1.5_ALPHA

C++ C++

C++

c

372

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

ACE 1' 2 to schemat open-source oferujący wiele komponentów i wzorców służących do tworzenia wydajnych, rozproszonych, działających w czasie rzeczywistym i osadzonych systemów. ACE zapewnia dostęp do oferujących ogromne możliwości i jednocześnie wydajnych abstrakcji dla gniazd, pętli demultipleksujących, wątków oraz synchroni­ zowanych konstrukcji podstawowych. Projekt Apache'*'4 to wspólny wysiłek programistyczny wielu osób, którego celem jest utworzenie niezawodnej, niekomercyjnej, oferującej pełną gamę usług i powszechnie dostępnej implementacji serwera HTTP (WWW). ArgoUML 5,6 to oferujące duże możliwości i łatwe w użyciu graficzne środowisko projektowania oprogramowania, które obsługuje etap projektowania, rozwoju oraz dokumentowania aplikacji obiektowych. Apache Cocoon7'8 to szkielet publikacji w języku XML, który przenosi użycie technik XML oraz XSLT w przypadku aplikacji serwerowych na nowy poziom jakości. Cocoon zaprojektowano z myślą o wydajności i skalowalności przy użyciu potokowego przetwa­ rzania standardu SAX. Oferuje on elastyczne środowisko bazujące na rozdzieleniu obsza­ rów zainteresowania między treścią, logiką i stylem. Scentralizowany system konfigu­ racji oraz wyrafinowane techniki buforowania dopełniają obrazu całości i pomagają w tworzeniu, wdrażaniu i konserwowaniu niezawodnych aplikacji serwerowych XML. DemoGL9' 10 to oparta na technice OpenGL i jeżyku C++ platforma wykonawcza Win32 dla efektów audiowizualnych. Pozwala na tworzenie samodzielnych plików wykony­ walnych lub wygaszaczy ekranu na platformie Microsoft Windows. HSQL Database Engine"' 12 (HSQLDB) to silnik relacyjnej bazy danych napisany w języku Java z użyciem sterownika JDBC, obsługujący podzbiór standardu ANS1-92 SQL. Oferuje niewielki (liczący około 100 KB), szybki silnik bazy danych obsługujący tabele przechowywane w pamięci i na dysku.

1 http: //www. cs. w ustl.ed u /~ sclm id l/A C E .h tm l.

1 ace. 3 http://httpd.apache.org/. 4 apache. 5 http://argoum l. tigris. org/.

6 argouml. I http: //xml. apache, org/cocoon/. 8

cocoon. h ttp://sourceforge.net/projects/dem ogl.

10 dem ogl. II http://h sqldb.sourceforge.net/.

12 hsqldb.

Dodatek A ♦ Struktura dołączonego kodu

373

NetBSD 1'1' 14 to darmowy, bezpieczny i przenośny uniksowy system operacyjny dostęp­ ny na wielu platformach, od 64-bitowych maszyn AlphaServer i systemów biurkowych po urządzenia przenośne i osadzone. Jego przejrzysty projekt oraz zaawansowane funk­ cje sprawiają, że stanowi doskonały wybór zarówno w przypadku środowisk produk­ cyjnych, jak i badawczych. Ponadto dostępny jest jego pełny kod źródłowy. OpenCL 15,16 to (przynajmniej w zamierzeniach) przenośna, łatwa w użyciu i wydajna biblioteka klas języka C++ związanych z kryptografią. Perl17 l!i' 19 to język zoptymalizowany pod względem przetwarzania dowolnych plików tekstowych, wydobywania z nich informacji oraz drukowania na ich podstawie raportów. Znakomicie nadaje się również do użycia w przypadku wielu zadań związanych z za­ rządzaniem systemem. W założeniach ma on być praktyczny (łatwy w użyciu, wydajny, pełny), a nie piękny (niewielki, elegancki, minimalny). Purenum20' 2' to biblioteka obsługi wielkich liczb całkowitych napisana w języku C++. Jej typ Integer o nieograniczonej szerokości działa z wszystkimi operatorami mate­ matycznymi obsługiwanymi przez C++, ale w przeciwieństwie do typu int, nigdy nie powoduje błędów przepełnienia. Dzielenie przez zero i wyczerpanie pamięci to prze­ chwytywane wyjątki. Używany jest zoptymalizowany kod typu inline i programowe operacje na typie Integer dla wartości o pojedynczej szerokości działają niemal tak samo szybko, jak sprzętowe operacje na typie int. Typ Array zapewnia dostęp do tablic wielkich liczb całkowitych o zmiennym rozmiarze. QtChat22 23 to klient usługi Yahoo! Chat oparty na bibliotece Qt. Zawiera takie funkcje jak bogate opcje automatycznego ignorowania, podświetlanie, przesyłanie komunikatów prywatnych i inne. Biblioteka socket+ + 24,25 definiuje rodzinę klas języka C++, które mogą być używane bardziej wydajnie w przypadku stosowania gniazd w porównaniu z bezpośrednim wywoływaniem niskopoziomowych funkcji systemowych. Jedną z istotnych korzyści

13 http://www.nelbsd.org/. 14 netbsdsrc. 15 http://opencl.source/orge. net/. 16 OpenCL 17 http://www.perl.org/.

18http://www.cpan. org/. 19p éri

20 http://www.catlikegames. cont/purenum/. 21 purenum. 22 http://safariexamples.informit.eom/0201799405/citchat/src/0.9.7/. 23 qtchat. 24 http://www. netsw. org/softeng/lang/c+ +/libs/socket+ +/.

25 socket.

374

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

związanych z użyciem Socket++jest to, że oferowany interfejs jest taki sam jak w przy­ padku klasy iostream, więc użytkownicy mogą wykonywać bezpieczne pod względem typów operacje wejścia-wyjścia. Tomcat 4 20' 2 to oficjalna implementacja referencyjna (Reference Implementation) technologii Servlet 2.3 oraz JavaServer Pages 1.2. Visual Component Framework 28' 29 to schemat strukturalny zapisany w języku C++ i zaprojektowany jako pełny międzyplatformowy szkielet interfejsów GUI. Jego utwo­ rzenie zostało zainspirowane łatwością użycia środowisk takich jak NEXTStep’s Inter­ face Builder, zintegrowanych środowisk tworzenia aplikacji w języku Java takich jak JBuilder. Visual J++ oraz produktów firmy Borland — Delphi oraz C++ Builder. X Window System 30,31 to niezależny od producenta i architektury systemu oraz sie­ ciowo przezroczysty system okienkowy oraz standard interfejsu użytkownika. System X Window działa na bardzo wielu maszynach obliczeniowych i graficznych. /

W oddzielnym katalogu32 zawarto również formatowane strony dokumentacji, do których odwołania występują w tekście książki.

26 http://jakaria.apache.org/tomcat/. 27 J,4. 28 http://vcf.sourceforge.net/.

29vcf. 30 http://www.x.org/. 31 Xfree86-3.3. 32 doc.

Dodatek B

Podziękowania dla autorów kodu źródłowego Kod źródłowy wykorzystany w przykładach zawartych w niniejszej książce stanowi wkład wniesiony w wiele inicjatyw społeczności twórców kodu open-source, których wymieniono poniżej. Rich Salz, AGE Logic, Inc., Eric Allman, Kenneth Almquist, American Telephone and Telegraph Co., Apache Software Foundation, Kenneth C. R. C. Arnold, Anselm Baird-Smith, Graham Barr, Berkeley Softworks, Jerry Berkman, Keith Bostic, Frans Bouma, Manuel Bouyer, Larry Bouzane, John H. Bradley, John Brezak, Brini, Mark Brinicombe, University of British Columbia, Regents of the University of California, Ralph Campbell, Carnegie Mellon University, Scott Chasin, James Clark, Aniruddha Gokhale, J. T. Conklin, Donna Converse, Robert Paul Corbett, Gregory S. Couch, Jim Crafton, Charles D. Cranor, Ian F. Darwin, Christopher G. Demetriou, Peter Dettori, Digital Equipment Corporation, Derek Dominish, Leendert van Doom, Kinga Dziem­ bowski, Julian Elischer, Peter Eriksson, University o f Erlangen-Nuremberg, Robert S. Fabry, Kevin Fall, Danno Ferrin, Michael Fischbein, Alessandro Forin, Free Software Foundation, Inc., Thorsten Frueauf, Fujitsu Limited, Jim Fulton, John Gilmore, Eric Gisin, Michael Graff, Susan L. Graham, Bradley A. Grantham, Matthew Green, David Greenman, Jarle Greipsland, Neil M. Haller, Charles M. Hannum, Hewlett-Packard Company, Hitachi, Ltd., Ken Hornstein, Steve Hotz, HSQL Development Group, Con­ rad C. Huang, Jason Hunter, Board of Trustees of the University o f Illinois, Imperial College of Science, Technology & Medicine, International Business Machines Corp., Internet Software Consortium, Institute of Electrical and Electronics Engineers, Inc., Van Jacobson, Mats O. Jansson, Anthony Jenkins, David Jones, William N. Joy, And­ reas Kaiser, Philip R. Karn, Peter B. Kessler, Chris Kingsley, Steve Kirkendall, Thomas Koenig, John T. Kohl, Andreas König, Anant Kumar, Lawrence Berkeley Laboratory, Joe O'Leary, Samuel J. Leffler, Dave Lemke, Ted Lemon, Craig Leres, Kim Letkeman, Jack Lloyd, Mach Operating System project at Carnegie-Mellon University, Rick Maćkiem, Remy Maucherat, Stefano Mazzocchi, Steven McCanne, Craig R. McClanahan, Rob McCool, Peter Mcllroy, Marshall Kirk McKusick, Eamonn McManus, Luke Mewburn, Paul Mockapetris, Andrew Moore, Rajiv Mordani, Thomas Mueller,

376

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Keith Muller, Mike Muuss, Ron Natalie, Philip A. Nelson, NetBSD Foundation, Inc.. Network Computing Devices, Inc., Novell, Inc., Mark Nudelman, Jeff Okamoto, Arthur David Olson, Joseph Orost, Keith Packard, Kirthika Parameswaran, David Parsons, Jan-Simon Pendry, Chris D. Peterson, Jochen Pohl, Paul Popelka, Harish Prabandham, Quadcap Software, Theo de Raadt, Mandar Raje, Michael Rendell, Asa Romberger, Mark Rosenstein, Gordon W. Ross, Guido van Rossum, Douglas C. Schmidt, Donn Seeley, Margo Seltzer. Roger L. Snyder, Wolfgang Solfrank, Solutions Design, Henry Spencer, Diomidis Spinellis, Davanum Srinivas, Stichting Mathematisch Centrum, Timothy C. Stoehr, Sun Microsystems, Inc., Gnanasekaran Swaminathan, Ralph R. Swick, Robert S. Thau, Spencer Thomas, Jason R. Thorpe, TooLs GmbH, Chris Torek, TRW Financial Systems, John Tucker, University of Utah, Unix System Laboratories, Inc., Anil K. Vijendran, Lance Visser, Vixie Enterprises, Paul Vixie, Vrije Universiteit Amsterdam, John S. Walden, Larry Wall, Sylvain Wallez, Edward Wang, Washington University. Niklaus Wirth, John P. Wittkoski, James A. Woods, X Consortium, Inc., Shawn Yarbrough, Ozan Yigit, Erez Zadok, Ilya Zakharevich, Carsten Ziegeler oraz Christos Zoulas.

Dodatek C

Pliki źródłowe Poniższa lista zawiera uporządkowane alfabetycznie pełne ścieżki dostępu do wszyst­ kich plików, o których była mowa w treści książki. Główna część nazwy każdego pliku (na przykład cat.ć) występuje w indeksie. ace. 372 ace/ace. 308, 315, 3 17 ace/ace/Addr.i, 290 ace/ace/Cleanup_Strategies_T. h, 242 ace/ace/Dump.h, 290 ace/ace/FutureSel.h, 287 ace/ace/Min_Max. /;, 295, 296 ace/ace/OS.cpp, 296 ace/apps/drwho/PMC_Ruser.cpp, 346 ace/bin/pippen.pl, 292 ace/TAO/examples/mfc/server.cpp. 3 10 ace/TAO/tao/Sequence_T.cpp, 239 ace/TA O/taofTA OSingleton. h. 311 ace/TA 0/tests/Param_Test/any. cpp. 286 ace/TA O/tests/Param Test/any. h. 286 apache, 372 apache/src/ap/apsnprintf.c, 221 apache/src/ap/Make/lle.tmpl, 227 apache/src/indude/multithread.h, 158 apache/src/lib/expal-lile, 301 apache/src/lib/sdbm, 301 apache/src/main/lutp_core.c, 256 apache/src/main/hltp protocol, c, 231, 256 apache/src/Makefde.tmpl, 188 apache/src/makefde.win, 189

apache/src/modules/standard/mod so.c. 302 apache/src/os/netware/multilhread.c, 1 58 apache/src/os/win32/multilhread.c. 158 apache/src/os/win32/util_win32.c, 22 1 apache/src/regex, 301 argouml, 372 argouml/org/argouml/application/events/ ArgoEvent.java, 273 argoumUorg/argouml/application/events/ ArgoModuleEvent.java, 273 argouml/org/argouml/application/events/ ArgoNotationEvent.java, 273 argouml/org/argouml/cognitive/critics/ Critic.java, 281 argouml/org/argouml/ui/ActionGoToEdit. java, 281 argouml/org/argouml/ui/TabResultsjava, 271 argouml/org/argoumUuml/diagram/ui./ FigEdgeModelElementjava. 285 cocoon, 372 cocoon/src, 261 cocoon/src/java/org/apache/cocoon/ components/language/programming/ java/Javacjava, 44 cocoon/src/java/org/apache/cocoon/ components/renderer/ExtendableRende *»rerFactoryjava. 311

378

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

cocoon/src/java/org/apache/cocoon/ components/sax/XMLByteStreamComp *+iler.java, 281 cocoon/src/java/org/apache/cocoont Maln.java. 66 cocoon/src/java/org/apache/cocoon/ transformation'LDAPTransformer.java. 324 cocoon/src/java/org/apache/cocoon/ transformation/SQLTransformer.java. 150 cocoon/src/java/org/apache/cocoon/util/ PostlnputStream.java. 283 cocoon/src/java/org/apache/cocoon/utH/ StringUtils.java, 47 cocoon/src/sçratchpad/src/org/apache/ cocoon/treeprocessor/MapStackPesolver. java, 5 1 demogl. 372 demogVCPP/DemoGL/dgl_dllstartupdia *+log.cpp, 271 demogl/lnclude/Misc/StdString. h. 290 doc. 374 doc/apropos.pdf, 241 doc/at.pdf 238 doc/cat.pdf 231,238 doc/ctags.pdf 238 doc/execve.pdf 212 doc/ffs.pdf 231 doc/gprof.pdf. 233 doc/last.pdf 242 doc/mbufpdf 236 doc/perlguts.pdf 236 doc/rfc2068.txi. 232 doc/rfc793.txt, 234. 240 doc/sendmail.pdf 232 doc/strftime.pdf 238 doc/lcpdump.pdf 236 doc/vixie-security.pdf 235 hsqldb. 372 hsqldb/CHANGELOG.txt, 369 hsqldb/dev-docs/hsqldb/index.html. 236 hsqldb/doc/internet/hSql. hind, 356 hsqldb/doc/internet/hSqlSyntax.html, 369 hsqldb/doc/src/org/hsqldb/Library.html, 369 hsqldb/index.html. 355

hsqldb/src/build.bat, 356 hsqldb/src/ltsqldb/ServerConnection.java. 159 hsqldb/src/org/hsqldb/Cache.java. 221 hsqldb/src/org/hsqldb/Column.java, 297 hsqldb/src/org/hsqldb/Database.java. 222, 368 hsqldb/src/org/lisqldb/DatabaseInforma *+tion.java, 217 hsqldb/src/org/hsqldb/jdbcConnection.java, 287 hsqldb/src/org/hsqldb/jdbcDatabaseMela t+Dala.java, 287 hsqldb/src/org/hsqldb/jdbcPreparedState **ment.java, 204 hsqldb/src/org/hsqldb/Library.java, 357, 365. 366, 367, 368 hsqldb/src/org/hsqldb/Like.java, 297 hsqldb/src/org/hsqldb/Parser.java, 3 13 hsqldb/src/org/hsqldb/Tokenicer.java, 365 hsqldb/src/org/hsqldb/Trace.java. 366. 367 hsqldb/src/org/hsqldb/WebServer.java. 281 j4/catalina/src/sliare/org/apache/catalina/ core/StandardContext.java, 193 J/4, 374 jt4/buildproperties.sample. 191 jt4/calalina/src/share/org/apache/caialina. 336 jt4/calalina/src/share/org/apache/caialina' connector, 263 jt4/catalina/src/share/org/apaclie/catalina core/ContainerBase.java, 148, 149.270 jt4/catalina/src/share/org/apache/catalinaJ core/DefaultContext.java, 281 jt4/catalina/src/share/org/apache/catalmaJ core/StandardWrapper Valve.java. 206 jl4/catalina/'src/share/org/apache/catalina/ Host.java, 281 jt4/catalina/src/share/org/apache/cataUna/ LifecycleExceplion.java, 148 jt4/catalina/src/share/org/apacheZcataliiiaj loader/StandardLoaderjava. 65. 148. 149 jl4/catalina/src/share/org/apache/catalma/ sessionZJDBCStore.java. 147 jt4/catalina/src/share/org/apache/catalina/ startup/Catallna.java, 36

Dodatek C ♦ Pliki źródłowe

jt4/catalina/src/share/org/apacbe/calalina/ startup/CatalinaServicejava. 37 jl4/catalinwsrc/share/org/apache/catalina.' start up/Conte.xtConJig.java, 149 jt4/catalina/src/share/org/apache/catalina/ util/Queuejava, 160 jt4/catalina/src/share/org/apache/catalina/ valves, 262 jt4/catalina/src/share/org/apache/catalina/ valves/RemoteAddrValvejava, 262 jt4/catalina/src/share/org/apache/catalina' valves/RequestFilterValvejava. 261 jt4/catalina/src/test/org/apache/naming/ resources/BaseDirContextTestCasejava, 209 jt4/catalina/src/test/org/apache/naming/ resources/fVARDirContextTestCasejava, 209 jt4/jasper/src/share/org/apache/jasper/ compiler/JspReaderjava. 51. 150 jt4/jasper/src/share/org/apache/jasper/ logging/JasperLoggerjava, 158 jt4/jasper/src/share/org/apache/jasper/util/ FastDateFormatjava, 210 jt4/webapps/examples/jsp/num/numguessjsp, 304 jt4/webapps/examples/WEB-INF/classes/ num/NumberGuessBeanjava, 304 jt4/webapps/manager/build.xml, 189 netbsdsrc, 373 netbsdsrc/bin/cal/cat. 1, 231. 238 netbsdsrc/biwcat/cat.c. 38. 196. 231 netbsdsrc/bin/chio/chio.c, 102 netbsdsrc/bin/csh/func.c, 64. 279 netbsdsrc/bin/csh/lex.c, 125 netbsdsrc/bin/csh/parse.c, 124 netbsdsrc/bin/csh/set.c, 58 netbsdsrc/bin/csh/str.c, 64 nelbsdsrc/binJdate/dale.c, 219 rtetbsdsrc/bin/dd/misc.c, 161 netbsdsrc/bin/echo/echo.c, 34 nelbsdsrc/bin/ed, 166 netbsdsrc/binJed/main.c. 165 netbsdsrc/birt/expr/expr.c. 130, 147 netbsdsrc/bin/ksh/config.h, 191

379

netbsdsrc/bin/ksh/history.c, 63 netbsdsrc/bin/ksh/jobs.c, 123 netbsdsrc/bin/ksh/lex.c, 121.275 netbsdsrc/bin/ksh/main.c, 54 netbsdsrc/bin/ksh/var.c, 220 netbsdsrc/bin/ls/ls.c, 46, 97 netbsdsrc/bin/ls/ls.h, 97 netbsdsrc/bin/mv/mv.c. 95 netbsdsrc/bin/pax/cache.c, 75 netbsdsrc/bin/pax/options.c, 193, 293 netbsdsrc/bin/pax/pax.c. 55 netbsdsrc/bin/pax/pax.h, 293 netbsdsrc/bin/pax/tables.c, 117 netbsdsrc/bin/rcp/rcp.c. 157. 170 netbsdsrc/bin/rcp/utH.c, 157 netbsdsrc/bin/rmdir/rmdir. c, 34 5 netbsdsrc/bin/sh/alias.c, 122, 123.220 netbsdsrc/bin/sh/eval.c, 127 netbsdsrc/bin/sh/jobs.c, 143 netbsdsrc/bin/sh/nodetypes, 142 netbsdsrc/bin/sh/output.c, 81 netbsdsrc/bin/sh/parser.c. 144 netbsdsrc/bin/sh/var.c, 63 netbsdsrc/bin/stty/key.c, 115, 130 netbsdsrc/bin/stty/print.c, 76 netbsdsrc/distrib/miniroot/upgrade.sh. 34 netbsdsrc/distrib/utils/libhack/getpwent. c. 83 netbsdsrc/distrib/utils/more. 25. 163 netbsdsrc/etc/inetd.conf. 306 netbsdsrc/games/backgammon/ backgammon/move.c, 206 netbsdsrc/games/battlestar/parse.c, 126 netbsdsrc/games/gomoku/pickinove. c. 103, 127 netbsdsrc/games/hack/hack.objnam.c. 52 netbsdsrc/games/hunt/hunt/hunt.c. 146 netbsdsrc/games/pom/pom.c, 358. 359. 361. 362, 363 netbsdsrc/games/rogue/curses. c. 82 netbsdsrc/games/rogue/random. c, 82 netbsdsrc/games/rogue/room. c. 81 netbsdsrc/games/rogue/save.c, 103

380

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

netbsdsrc/games/snake/snake/snake. c, 85 netbsdsrc/games/worms/worms. c, 58 nelbsdsrc/include/db.h, 99 netbsdsrc/include/protocols/dumprestore.h, 255 netbsdsrc/include/protocols/routed.h, 255 netbsdsrc/include/protocols/rwhod.h, 255 netbsdsrc/indude/prolocols/talkd. h, 254 netbsdsrc/include/protocols/timed.h. 255 netbsdsrc/lnclude/rpc/rpc_msg. h, 90 netbsdsrc/include/stdio.h, 280 netbsdsrc/include/tzfile. h. 360 nelbsdsrc/lib/libc. 300 netbsdsrc/lib/libc/arch/i386/gen/frexp. c. 92 netbsdsrcAib/libc/db, 298 netbsdsrc/lib/libc/db/btree, 127 netbsdsrc/lib/libc/db/hash/page.h. 243 netbsdsrcdib/hbc/db/recno/extem.h, 99 netbsdsrc/lib/libc/gen/_errno.c1300 nelbsdsrc/lib/ltbc/net/getservent.c, 56 netbsdsrc/lib/libc/quad/muldi3.c. 233 nelbsdsrc/lib/libc/regex/engine.c, 168. 207 nelbsdsrc/lib/libc/regex/regcomp.c, 169 netbsdsrc/lib/libc/regex/utds.h, 280 netbsdsrc/lib/libc/stdio/fwrite.c, 264 netbsdsrc/lib/libc/stdlib/bsearch.c. 66. 300 netbsdsrc/lib/libc/stdlib/malloc.c, 90. 168 netbsdsrc/lib/libc/stdlib/malloc. h, 89 neibsdsrc/lib/libc/stdlib/qsort.c, 351 netbsdsrc/Ub/libc/stdlib/radixsort.c, 70 netbsdsrc/lib/libc/string/strcspn. c. 220 netbsdsrc/lib/libc/string/strlen. c, 82 netbsdsrc/lib/libc/time/localtime.c, 115 nelbsdsrc/lib/libc/time/strfiime.3, 238 tielbsdsrcdib/libc/time/zic. c\ 313 netbsdsrc/lib/libc/yp/xdryp.c, 258 netbsdsrc/lib/libc/yp/ypJirst.c. 258 netbsdsrc/lib/libcompat/4.3/lsearch.c, 297 rietbsdsrc/lib/libcompat/regexp/regexp.c, 220 netbsdsrc/lib/libcrypt. 300 nelbsdsrc/lib/libcurses, 300 netbsdsrc/lib/libcurses/tty.c, 81

netbsdsrc/lib/libedit, 300 netbsdsrc/lib/libedit/term. c, 54 nelbsdsrc/lib/libkvm. 300 netbsdsrc/lib/libntp/Makefile, 226 netbsdsrc/lib/libntp/systime. c. 206 netbsdsrc/lib/libpcap, 300 netbsdsrc/Hb/libpcap/gencode.h. 132 netbsdsrc/lib/libpcap/optimize. c. 133 netbsdsrc/lib/libutd/pty. c, 83 netbsdsrc/lib/libz/infblock.c, 127 nelbsdsrc/lib/libz/zutil.h. 280 netbsdsrc/libexec/atrun/atrun.c. 156 neibsdsrc/libexec/ßpd/fipd.c, 80 netbsdsrc/libexec/identd/idenid.c, 77. 83 netbsdsrc/libexec/identd/parse. c, 206 netbsdsrc/libexec/identd/version.c, 83 netbsdsrc/libexec/makewhatis/ makewhatis.sh, 157, 259 netbsdsrc/libexec/rpc. rusersd/nisersproc. c. 98 netbsdsrc/libexec/telnetd/defs. h, 98 netbsdsrc/regress/lib/libc/ieeefp/rouncb round, c, 208 netbsdsrc/regress/sys/kern/execve/doexec.c, 211

netbsdsrc/regressfsys/kerrvexecveigood/ nonexistshell. 211 netbsdsrc/regress/sys/kern/execve/MakeJlle. 211 netbsdsrc/regress/sys/kern/execve/tesls/ nonexistshell, 211 netbsdsrc/sbin/fsckJpreen.c. 74 netbsdsrc/sbin/init/init.c, 49 netbsdsrc/sbirdmount nfs/mount nfs.c, 315 netbsdsrc/sbin/mount portal/conf.c, 99 netbsdsrc/sbin/ping/ping. c, 76 netbsdsrc/sbin/restore/tape.c. 78. 151 netbsdsrc/sbin/route, 315 netbsdsrc/sbin/routed, 315 nelbsdsrc/sbin/routed/radix. c, 127 netbsdsrc/sbin/routed/rdisc.c, 78. 105 netbsdsrc/sbin/routed/lrace. c, 78 netbsdsrc/share/doc/smmJQ5.fastfs. 231

Dodatek C ♦ Pliki źródłowe

381

netbsdsrc/share/man/man9/mbuf. 9, 236

netbsdsrc/sys/dev/ic/ncr5380sbc.c, 56

netbsdsrc/share/misc/style, 61

netbsdsrc/sys/dev/isa/sbdsp. c. 266

netbsdsrc/src/games/arilhmetic/arithmetic.c,

netbsdsrc/sys/dev/isapnp/isapnpres.c. 127

161

netbsdsrc/sys/dev/q/isa/o/lsa.c. 96

nelbsdsrc/sys. 160, 278. 326. 331. 347

netbsdsrc/sys/dev/pci/if_fxp. c. 266

netbsdsrc/sys/arch. 180

netbsdsrc/sys/dev/pci/ifjxpreg. h, 86

nelbsdsrc/sys/arch/alpha/alpha/pmap. c, 207

netbsdsrc/sys/dev/pci/ncr.c. 266

netbsdsrc/sys/arch'alpha/include/types. h, 99

netbsdsrc/sys/dev/rnd.c, 113

neibsdsrc/sys/arch/amiga/dev/ser.c, 63

netbsdsrc/sys/dev/scsipi/scsipi base.c. 265

netbsdsrc/sys/arch/arm/arm32/ofw/ofw.c,

netbsdsrc/sys/dev/scsipi/sd. c, 265

155

netbsdsrc/sys/arch/arm32/arm32/stubs.c, 123, 124.'126

netbsdsrc/sys/arch/arm32/conf/GENERIC. 201

netbsdsrc/sys/arch/arm32/include/endian.h. 99

nelbsdsrc/sys/arch/arm32/isaif_csjsa.c. 218

netbsdsrc/sys/dev/tc/asc. c, 266 netbsdsrc/sys/dev/tc/px.c. 266 netbsdsrc/sys/kern/inil main.c, 273 netbsdsrc/sys/kern/kern descńp. c, 218 netbsdsrc/sys/kern/kernjkm.c. 302 netbsdsrc/sys/kern/kern synch. c. 242 netbsdsrc/sys/kern/sys_generic. c, 264

netbsdsrc/sys/arch/bebox/indude/bus. h. 171

netbsdsrc/sys/kern/syscalls.master. 264. 333

netbsdsrc/sys/arch/beboxJisa.łif_ed. c. 102

netbsdsrc/sys/kern/vfs_bio.c, 265. 266

netbsdsrc/sys,'arch/bebox/isa/pcvt/Util/ vttest, 211

netbsdsrc/sys/kern/vfs subr.c. 88. 221

netbsdsra'sys/arch/i386/i386/vm86.c. 170 nelbsdsrc/sys/arch/i386/isa'vector. i-, 265 netbsdsrc/sys/arch/i386/netboot/ne2100. c. 208

netbsdsrc/sys/arch/i386/netboot/wd80x3.c, 333

netbsdsrc/sys/arch/i386/stand/lib/netif/ wd80x3.c. 333 netbsdsrc/sys/ardvmac68k/dev/adb., 113 netbsdsrc/sys/arch/mac68k/dev/adb.c. 113 netbsdsrc/sys/arch/mac68k/dev/adb_direct.c. 112, 113'

netbsdsrc/sys'arch/nups/mips/elfc, 345

netbsdsrc/sys/kern/vfs_vnops.c, 264 netbsdsrc/sys/kern/vnode Jf.c. 294 netbsdsrc/sys/kern/vnode Jf.h. 294 netbsdsrc/sys/kern/vnode J/.src. 264. 265. 294

netbsdsrc/sys/miscfs/nullfs, 309 netbsdsrc/sys/miscfs/specfs/spec_vnops.c, 265

netbsdsrc/sys/miscfs/union/union_vnops.c. 266

netbsdsrc/sys/msdosfs/bpb.h, 87 netbsdsrc/sys/msdosfs/msdosfs_vnops. c. 266 netbsdsrc/sys/net, 316 netbsdsrc/sys/net/route.c. 315

netbsdsrc/sys/arch/mvme68k/mvme68k/ machdep.c, 37

netbsdsrc/sys/netatalk, 177

netbsdsrc/sys/arch/pica/pica/machdep. c. 84

netbsdsrc/sys/netccitt/pk_subr. c, 243

netbsdsrc/sys/arch/powerpc/stand/of\vboot/ alloc.c, 207

netbsdsrc/sys/netinet. 177. 316

netbsdsrc/sys/arch/sparc/fpu/fpu jq rt.c , 321

netbsdsrc/sys/netinet/if atm. c, 16

netbsdsrc/sys/netinet/if arp.c, 221

netbsdsrc/sys/dev/ic/aha.c. 265

netbsdsrc/sys/netinet/in _prolo.c, 116

netbsdsrc/sys/dev/ic/cs4231reg. h. 243

netbsdsrc/sys/netinet/tcp.h. 87

netbsdsrc/sys/dev/ic/i82365. c. 266

netbsdsrc/sys/netinet/tcp Jsm .h, 233

382

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

nelbsdsrc/sys/neliso. 177, 3 16 netbsdsrc/sys/netnatm, 177, 316 nelbsdsrc/sys/nfe/nfsj/nops. c. 266 neibsdsrc/sys/sys/domain.h. 87 nelbsdsrc/sys/sys/ßle.h, 88 neibsdsrc/sys/sys/queue. h. 299 netbsdsrc/sys/sys/rnd.h. 280 nelbsdsrc/sys/sys/vnode.h. 294 neibsdsrc/sys/ufs/ext2fi/ext2fs_readwrite.c. 265 netbsdsrc/sys/ufs/ext2fs/ext2fs_vnops. c, 266 nelbsdsrc/sys/ufs/ffs. 231 netbsdsrc/sys/ufs/ffs/ffs_ vnops. c. 294 netbsdsrc/sys/ufs/ufe/ufs_vnops.c, 265. 266 netbsdsrc/sys/vm/vm_glue. c. 273 neibsdsrc/sys/vm/vm_swap.c, 168, 169 netbsdsrcJusr.bin/apply/apply.c, 48 nelbsdsrc/usr.bin/apropos/apropos. / , 241 netbsdsrc/usr.bin/at, 134 nelbsdsrc/usr.bin/al/at. 1, 238 nelbsdsrc/usr. bin/at/at. c, 46 netbsdsrc/usr.bin/cal/cal.c, 48 nelbsdsrc/usr.bin/checknr/checknr.c. 225 nelbsdsrc/usr. bin/cksum/cksum. c, 22 5 nelbsdsrc/usr. bin/compress/iopen.c. 48, 76, 84 nelbsdsrc/usr.bin/ctags/ctags. 1. 238 nelbsdsrc/usr. bin/ctags/ctags. c, 127 netbsdsrc/usr.bin/cut/cut. I, 246 nelbsdsrc/usr.bin/elJ2aout/elf2aoul.c, 36 netbsdsrc/usr.bin/error/pi.c. 51 nelbsdsrc/usr. bin/expand/expand. c, 39 nelbsdsrc/usr. bin/file/ascmagic. c, 36 netbsdsrc/usr.bin/file/tar.h, 9 1 netbsdsrc/usr.bin/find/find.c, 127 nelbsdsrc/usr. bin/fsplit/fsplit. c, 48 nelbsdsrc/usr. bin/ftp/complete. c, 48 nelbsdsrc/usr. bin/ftp/ulil. c. 21 nelbsdsrc/usr. bin/gprof/arcs. c. 233 netbsdsrc/usr.bin/gprof/gprof.c. 80. 134 nelbsdsrc/usr. bin/gprof/PSD. doc/postp. me, 233 nelbsdsrc/usr.bin/indent/args.c, 192

netbsdsrc/usr.bin/last/lasl. 1. 242 nelbsdsrc/usr. bin/less/less/version, c. 83 nelbsdsrc/usr.bin/lorder/lorder.sh. 245 nelbsdsrc/usr. bin/m4/eval. c, 44 netbsdsrc/usr.bin/m4/mdef.h, 243 nelbsdsrc/usr.bin/mail/list.c, 111 nelbsdsrc/usr. bin/make/make. h. 136. 138 nelbsdsrc/usr. bin/named/dig/dig. c, 86 nelbsdsrc/usr.bin/printf/printf.c. 341, 351 nelbsdsrc/usr. bin/rdist/docmd. c. 123 nelbsdsrc/usr.bin/rup/rup.c. 94, 121 nelbsdsrc/usr.bin/sed, 323 nelbsdsrc/usr. bin/sed/misc. c, 94 nelbsdsrc/usr. bin/sed/process.c, 314 nelbsdsrc/usr. bin/skeyinit/skeyinit. c. 95 nelbsdsrc/usr. bin/telnet/commands.c, 124 nelbsdsrc/usr. bin/tn32 70/api/asiosc. c, 203 nelbsdsrc/usr. bin/tn3270/api/dispasc. c, 203 netbsdsrc/usr.bin/tn3270/ctrl//unction.c, 203 netbsdsrc/usr.bin/tn3270/ctrl/funcHon.h. 203 nelbsdsrc/usr. bin/ln3270/ctrL/hostctlr. h, 203 nelbsdsrc/usr. bin/tn3270/ctrl/inbound, c. 203 nelbsdsrc/usr. bin/tn3270/ctrl/unix. kbd. 203 nelbsdsrc/usr. bin/tn32 70/tools/mkastods/ mkastods.c, 203 nelbsdsrc/usr. bin/tn3270/tools/mkastosc mkasiosc.c, 203 nelbsdsrc/usr. bin/tn32 70/lools/mkdsloas1 mkdsloas.c, 203 nelbsdsrc/usr. bin/ln32 70/tools/mkhits/ mkhits.c, 203 netbsdsrc/usr.bin/tsorl, 353 netbsdsrc/usr.bin/lsort/tsort.c. 132. 137 nelbsdsrc/usr.bin/vi, 277, 327. 331 nelbsdsrc/usr. bin/vi/kalalog/README. 325 nelbsdsrc/usr. bin/vi/vi/v increments. 277 nelbsdsrc/usr. bin/vi/vi/vs smap.c, 48 nelbsdsrc/usr.bin/wc/wc.c, 267, 350 nelbsdsrc/usr. bin/window, 254, 278 nelbsdsrc/usr.bin/xlint/linll/cgram.y, 130 nelbsdsrc/usr.bin/xlint/lint I/lint 1.h. 130 nelbsdsrc/usr. bin/yacc/defs. It. 167 nelbsdsrc/usr. bin/yes/yes. c. 25

Dodatek C ♦ Pliki źródłowe

netbsdsrc/usr. sbin/amd/amd/amd. c. 208 netbsdsrc/usr.sbin/amd/doc/am-utils.texi, 248 netbsdsrc/usr. sbinJamd/hlfsd/homedir. c, 94 netbsdsrc/usr.sbin/boolpd/hash.c, 217 netbsdsrc/usr.sbin/chown/chown.c, 2 18 netbsdsrc/usr.sbin/conftg, 201, 205 netbsdsrc/usr.sbin/dhcp, 254 netbsdsrc/usr. sbin/dhcp/server/confpars.c. 313 netbsdsrc/usr.sbin/lpr/lpd. 254 netbsdsrc/usr.sbin/named/doc. 235 netbsdsrc/usr. sbiwnamed/doc/misc/ vixie-security.ps, 235 netbsdsrc/usr.sbin/named/named/dbjoad.c, 313 netbsdsrc/usr. sbin/named/named/ dbjipdate.c, 234 netbsdsrc/usr. sbin/named/named/ ns_validate.c, 93 netbsdsrc/usr. sbin/named/named/tree, c, 128. 129 netbsdsrc/usr. sbin/named/named/tree. li. 127 netbsdsrc/usr.sbin/nfsd. 254 netbsdsrc/usr. sbin/pr/pr. c. 107 netbsdsrc/usr.sbin/quot/quot. c, 106 netbsdsrc/usr.sbin/sendmail/doc/intro, 232 netbsdsrc/usr.sbin/sendmail/src. 232 netbsdsrc/usr. sbin/tcpdump/print-icmp.c, 106 netbsdsrc/usr. sbin/tcpdump/tcpdump. 8. 236 netbsdsrc/usr.sbin/timed/timed/timed. c. 44 netbsdsrc/usr.sbin/xntp/xntpd/ntpjo.c, 193 netbsdsrc/usr.sbin/ypserv/ypserv/ypserv.c, 258 OpenCL. 373 OpenCUchecks/blockxpp. 281, 283 OpenCL/include/crc24.h, 288 OpenCL/include/crc32.lt. 283 OpenCUinclude/md2.h. 288 OpenCUinciude/openci.h. 288 OpenCL/include/pgp_s2k.h, 288 OpenCUinclude/skipjcick.h, 281 OpenCUsrc/pgp s2k.cpp, 288

383

perl, 373 perl/ext/DynaLoader/dlutils. c, 302 perl/ext/IO/lib/IO/Socket.pm. 293 perl/lib/CPAN.pm, 282 perl/lib/DirHandle.pm. 291 perl/lib/unicode/mktables. PL. 50 perl/os2/OS2/REXX/DLUDLL.pm, 292 perl/os2/os2tliread. h. 158 perl/pod, 205 perl/pod/perlguts.pod, 236 perl/t/op/array.t, 205 perl/t/TEST, 204, 205 perl/thread.b. 158 perl/utils/perlbug. PL. 212 perl/win32/Makeftle. 191 perl/win32/win32thread.c, 158 perl/win32/win32thread, li, 158 purenum, 373 purenum/integer.h, 290 qtchat. 373 qtchat/src/0.9.7/core/chatter.h, 287 qtchat/src/0.9.7/Tree/Menu.cc. 342 socket, 373 socket/ForkC. 162 socket/smtp. C, 289 socket/sockunix.C, 285 tools/codefind.pl, 27 vcf 374 vcf/src, 309 Xfree86-3.3, 374 XFree86-3.3/contrib/programs/ico/ico. c. 107, 153 XFree86-3.3/contrib/programs•/viewres/ viewres.c, 102 XFree86-3.3/contrib/programs/xcalc/ math.c, 1 10 Xfree86-3.3/conlrib/programs/xcalc/ xcalc.c. 193 XFree86-3.3/contrib/programs/xeyes/ Eyes.c, 219 XFree86-3.3/contrib/programs/xfontseU xfontsel.c, 270 XFree86-3.3/contrib/programs/xnian/ handler.c, 270

384

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

XFree86-3.3/conlrib/programs/xmanJ main.c. 270 XFree86-3.3/contrib/programs/xmaw Xman.ad, 270 XFree86-3.3/contrib/programs/xmessage. 309 XFree86-3.3/xc/config/imake, 202 XFree86-3.3/xc/conßg/makedepend. 202 XFree86-3.3/xc/config/util/mkshadow/ wildmat.c, 104 XFree86-3.3/xc/doc/specs/PEX5/PEX5.1/ Sl/xref.c, 75, 127 XFree86-3.3/xc/doc/specs/PEX5/PEX5.1/ Sl/xrefh. 75 XFree86-3.3/xc/include/extensions/ record, h, 97 XFree86-3.3/xc/inciude/Xpoll. h, 119, 120 XFree86-3.3/xc/include/Xthreads.h, 158 XFree86-3.3/xc/lib/fonUSpeedo/spglyph.c. 208 XFree86-3.3/xc/lib/font/util/atom.c. 117 XFree86-3.3/xc/lib/X11/quarks.c. 155 XFree86-3.3/xc/lib/Xl l/Xrm.c, 120 XFree86-3.3/xc/lib/Xt, 294 XFree86-3.3/xc/lib/Xl/GCManager.c, 68 XFree86-3.3/xc/lib/XT/Seleclion. c. 96

XFree86-3.3/xc/tib/xtrans/Xtransam.c. 154 XFree86-3.3/xc/programs/beforelight/ b4Hght.c, 112 XFree86-3.3/xc/programs/xieperf/ convolve, c, 109 XFree86-3.3/xc/programs/Xserver. 254 XFree86-3.3/xc/programs/Xserver/dix/ window.c, 127 XFree86-3.3/xc/programs/Xserver/hw'hp> input/drivers/hp7lc2k.c. 210 XFree86-3.3/xc/programs/Xserver/hwi xfree86/accel/cache/x/86bcache.c. 127 XFree86-3.3/xc/programs/Xserver/mi/ mivaltree.c, 127 XFree86-3.3/xc/programs/Xserver/PFX5' ddpex/mi/!evel2/miNCurve.c, 108 XFree86-3.3/xc/programs/Xserver/PFX5/ ddpex/mi/level2/miNurbs.c. 108 XFree86-3.3/xc/programs/Xserver/recordi set.c, 119 XFree86-3.3/xc/programs/Xserver/XprinV pd/PdText.c, 108 XFree86-3.3/xc/util/compress/compress. c, 111

XFree86-3.3/xc/util/patch./\lakefile. nt, 136. 185

Dodatek D

Licencje kodu źródłowego D .l. ACE Prawa autorskie do kodu źródłowego zawartego w katalogu ace zastrzega sobie Douglas C. Schmidt oraz jego grupa badawcza z Uniwersytetu Waszyngtońskiego. Kod ten jest rozpowszechniany z poniższą licencją. Copyright and Licensing Information for ACE(TM) and TAO(TH) [UACE(TM) and [2]TA0(TM) are copyrighted by [3]Douglas C. Schmidt and his [4]research group at [5]Washington University. Copyright (c) 1993-2001. all rights reserved. Since ACE and TAO are [6]open source. [7]free software, you are free to use. modify, and distribute the ACE and TAO source code and object code produced from the source, aslong as you include this copyright statement along with code built using ACE and TAO. In particular, you can use ACE and TAO in proprietary software and are under no obligation to redistribute any of your source code that is built using ACE and TAO. Note, however, that you may not do anything to the ACE and TAO code, such as copyrighting 1t yourself or claiming authorship of the ACE and TAO code, that will prevent ACE and TAO from being distributed freely using an opensource development model. ACE and TAO are provided as is with no warranties of any kind, including the warranties of design, merchantibility and fitness for a particular purpose, noninfringement, or arising from a course of dealing, usage or trade practice. Moreover. ACE and TAO are provided with no support and without any obligation on the part of Washington University. Its employees, or students to assist in its use. correction, modification, or enhancement. However, commercial support for ACE and TAO are available from [8]Riverace and [9]0C1. respectively. Moreover, both ACE and TAO are Y2K-comp11ant. as long as the underlying OS platform is Y2K-compliant. Washington University, its employees, and students shall have no liability with respect to the infringement of copyrights, trade secrets or any patents by ACE and TAO or any part thereof. Moreover, in no event will Washington University, its employees, or students be liable for any lost revenue or profits or other special, indirect and consequential damages.

386

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source The [10]ACE and [lljTAO web sites are maintained by the [12]Center for Distributed Object Computing of Washington University for the development of open source software as part of the [13]open source software community. By submitting comments, suggestions, code, code snippets, techniques (including that of usage), and algorithms, submitters acknowledge that they have the right to do so. that any such submissions are given freely and unreservedly, and that they waive any claims to copyright or ownership. In addition, submitters acknowledge that any such submission might become part of the copyright maintained on the overall body of code, which comprises the [14]ACE and [15]TA0 software. By making a submission, submitter agree to these terms. Furthermore, submitters acknowledge that the incorporation or modification of such submissions is entirely at the discretion of the moderators of the open source ACE and TAO projects or their designees. The names ACE (TH). TAO(TM). and Washington University may not be used to endorse or promote products or services derived from this source without express written permission from Washington University. Further, products or services derived from this source may not be called ACE(TM) or TAO(TM). nor may the name Washington University appear 1n their names, without express written permission from Washington University. If you have any suggestions, additions, comments, or questions, please let [16]me know. [17]Douglas C. Schmidt Back to the [18]ACE home page. References 1. 2. 3. A. 5. 6. 7 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18.

http://www.cs.wust!.edu/-schmidt/ACE.html http://www.cs.wust!.edu/-schmidt/TAO.html http://www.es.wustl.edu/-schmidt/ http://www.cs.wust!.edu/-schmidt/ACE-members.html http://www.wustl.edu/ http://www.opensource.org/ http://www.gnu.org/ http://www.riverace.com/ flle://localhost/home/cs/faculty/schmidt/.www-docs/www.oc1web.com http://www.cs.wustl.edu/-schmidt/ACE.html http://www.es.wustl.edu/-schmidt/TAO.html http://www.es.wustl,edu/~schmidt/doc-center.html http://www.opensource.org/ http://www.cs.wustl.edu/'Schmidt/ACE-obtain.html http://www.es.wustl.edu/-schmidt/TAO-obtain.html mailto:[email protected] http://www.cs.wust!,edu/-schmidt/ file://localhost/home/cs/faculty/schmidt/.www-docs/ACE.html

D.2. Apache Prawa autorskie do kodu źródłowego zawartego w katalogach apache, cocoon oraz jt4 zastrzega sobie organizacja Apache Software Foundation. Kod ten jest rozpowszechniany z poniższą licencją.

387

Dodatek D « Licencje kodu źródłowego

The Apache Software License. Version 1.1

Copyright (c) 2000 The Apache Software Foundation.

A U rights reserved.

Redistribution and use in source and binary forms. with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this 11st of conditions and the following disclaimer in the documentation and/or other materials provided with the distrlbution. 3. The end-user documentation included with the redistribution, if any. must include the following acknowledgment: "This product includes software developed by the Apache Software Foundation (http://www.apache.org/).' Alternately, this acknowledgment may appear in the software itself, if and wherever such third-party acknowledgments normally appear. 4. The names "Apache" and "Apache Software Foundation" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact [email protected]. 5. Products derived from this software may not be called "Apache", nor may "Apache" appear in their name, without prior written permission of the Apache Software Foundation. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT. INDIRECT. INCIDENTAL. SPECIAL. EXEMPLARY. OR CONSEQUENTIAL DAMAGES (INCLUDING. BUT NOT LIMITED TO. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES: LOSS OF USE. DATA. OR PROFITS: OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY. WHETHER IN CONTRACT. STRICT LIABILITY. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This software consists of voluntary contributions made by many individuals on behalf of the Apache Software Foundation. For more information on the Apache Software Foundation, please see .

D.3. ArgoUML Prawa autorskie do kodu źródłowego zawartego w katalogu ArgoUML zastrzegają sobie regenci Uniwersytetu Kalifornijskiego. Kod ten jest rozpowszechniany z poniższą licencją. Copyright (c) 1996-99 The Regents of the University of California. All Rights Reserved. Permission to use. copy, modify, and distribute this software and its documentation without fee. and without a written agreement is hereby granted, provided that the above copyright notice

388

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source and this paragraph appear in ail copies. This software program and documentation are copyrighted by The Regents of the University of California. The software program and documentation are supplied "AS IS", without any accompanying services from The Regents. The Regents does not warrant that the operation of the program will be uninterrupted or error-free. The end-user understands that the program was developed for research purposes and is advised not to rely exclusively on the program for any reason. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR OIRECT. INDIRECT. SPECIAL. INCIDENTAL. OR CONSEQUENTIAL DAMAGES. INCLUDING.LOST PROFITS. ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION. EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISEO OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS. AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE. SUPPORT. UPDATES. ENHANCEMENTS, OR MODIFICATIONS.

D.4. DemoGL Prawa autorskie do kodu źródłowego zawartego w katalogu DemoGL zastrzega sobie organizacja Solutions Design. Kod ten jest rozpowszechniany z poniższą licencją. DemoGL User License Copyright °1999-2001 Solutions Design. All rights reserved. Central DemoGL Website: www.demogl.com. Released under the following license: (BSD) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2 Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY SOLUTIONS DESIGN "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVEIIT SHALL SOLUTIONS DESIGN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT. INDIRECT. INCIDENTAL SPECIAL. EXEMPLARY. OR CONSEQUENTIAL DAMAGES (INCLUDING. BUT NOT LIMITED TO. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES: LOSS OF USE. DATA. OR PROFITS: OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY. WHETHER IN CONTRACT. STRICT LIABILITY. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained 1n the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Solutions Design.

D.5. hsqldb Prawa autorskie do kodu źródłowego zawartego w katalogu hsqldb zastrzega sobie orga­ nizacja HSQL Development Group. Kod ten jest rozpowszechniany z poniższą licencją.

Dodatek D ♦ Licencje kodu źródłowego

389

Copyright (c) 2001. The HSQL Development Group All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

Neither the name of the HSQL Development Group nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT. INDIRECT. INCIDENTAL. SPECIAL. EXEMPLARY. OR CONSEQUENTIAL DAMAGES (INCLUDING. BUT NOT LIMITED TO. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES: LOSS OF USE. DATA. OR PROFITS: OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY. WHETHER IN CONTRACT. STR!CT LIABILITY. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE This package is based cn HyperscnicSQL. originally developed by Thomas Mueller.

D.6. NetBSD Prawa autorskie do kodu źródłowego zawartego w katalogu nelbsd zastrzegają sobie regen­ ci Uniwersytetu Kalifornijskiego. Kod ten jest rozpowszechniany z poniższą licencją. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1 Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3 All advertising materials mentioning features or use of this software must display the following acknowledgement: This product includes software developed by Christopher G. Demetriou for the NetBSD Project. 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission THIS SOFTWARE IS PROVIOEO BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT. INDIRECT. INCIDENTAL. SPECIAL. EXEMPLARY. OR CONSEQUENTIAL DAMAGES (INCLUDING. BUT NOT LIMITED TO. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES: LOSS OF USE. DATA. OR PROFITS: OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY. WHETHER IN CONTRACT. STRICT LIABILITY. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

390

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

D.7. OpenCL Prawa autorskie do kodu źródłowego zawartego w katalogu OpenCL zastrzega sobie organizacja OpenCL Project. Kod ten jest rozpowszechniany z poniższą licencją. Copyright (C) 1999-2001 The OpenCL Project. All rights reserved. Redistribution and use in source and binary forms, for any use. with or without modification, is permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this 11st of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Products derived from this software may not be called 'OpenCL" nor may 'OpenCL' appear in their names without prior written permission of The OpenCL Project. THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) "AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR(S) OR CONTRIBUTOR«) BE LIABLE FOR ANY DIRECT. INDIRECT. INCIDENTAL. SPECIAL. EXEMPLARY. OR CONSEQUENTIAL DAMAGES (INCLUDING. BUT NOT LIMITED TO. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES: LOSS OF USE. DATA. OR PROFITS: OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY. WHETHER IN CONTRACT. STRICT LIABILITY. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

D.8. Perl Prawa autorskie do kodu źródłowego zawartego w katalogu perl zastrzega sobie Larry Wall. Kod ten jest rozpowszechniany z poniższą licencją. Perl Kit. Version 5.0 Copyright 1989-2001. Larry Wall All rights reserved. This program is free software: you can redistribute it and/or modify it under the terms of either: a) the GNU General Public License as published by the Free Software Foundation: either version 1. or (at your option) any later version, or b) the 'Artistic License" which comes with this Kit. This program is but WITHOUT ANY MERCHANTABILITY the GNU General

distributed in the hope that it will be useful, WARRANTY: without even the implied warranty of or FITNESS FOR A PARTICULAR PURPOSE. See either Public License or the Artistic License for more details.

1 Dodatek D ♦ Licencje kodu źródłowego The "Artistic License" Preamble The intent of this document Is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder as specified below. "Copyright Holder" is whoever is named in the copyright or copyrights for the package. "You” is you. if you're thinking about copying or distributing this Package. "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify 1t to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) "Freely Available" means that no fee is charged for the Item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way. provided that you Insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as uunet.uu.net. or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how 1t differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder.

391

392

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source 4. You may distribute the programs of this Package 1n object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (1n the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) give non-standard executables non-standard names, and clearly document the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. You may embed this Package's interpreter within an executable of yours (by linking): this shall be construed as a mere form of aggregation, provided that the complete Standard Version of the interpreter 1s so embedded. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whoever generated them, and may be sold commercially, and may be aggregated with this Package. If such scripts or library files are aggregated with this Package via the so-called "undump" or "unexec" methods of producing a binary executable image, then distribution of such an image shall neither be construed as a distribution of this Package nor shall 1t fall under the restrictions of Paragraphs 3 and 4. provided that you do not represent such an executable image as a Standard Version cf this Package. 7. C subroutines (or comparably compiled subroutines in other languages) supplied by you and linked into this Package in order to emulate subroutines and variables of the language defined by this Package shall not be considered part of this Package, but are the equivalent of input as in Paragraph 6. provided these subroutines do not change the language in any way that would cause it to fail the regression tests for the language. 8. Aggregation of this Package with a commercial distribution is always permitted provided that the use of this Package is embedded: that is. when no overt attempt is made to make this Package's interfaces visible to the end user of the commercial distribution. Such use shall not be construed as a distribution of this Package. 9. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. INCLUDING. WITHOUT LIMITATION. THE IMPLIED WARRANTIES OF MERCHANTI8ILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End

Dodatek D ♦ Licencje kodu źródłowego

393

D.9. qtchat Program qtchat został opracowany przez Antony’ego Jenkinsa oraz Chada Dixona. Jest on rozpowszechniany z licencją Qt Public Source Licence, dostępną pod adresem http://www.trolltech. com.

D.10. socket Prawa autorskie do kodu źródłowego zawartego w katalogu socket zastrzega sobie Gnanasekaran Swaminathan. Kod ten jest rozpowszechniany z poniższą licencją. Copyright (C) 1992-1996 Gnanasekaran Swaminathan Permission is granted to use at your own risk and distribute this software in source and binary forms provided the above copyright notice and this paragraph are preserved on all copies. This software is provided "as is' with no express or implied warranty.

D .ll. vcf Prawa autorskie do kodu źródłowego zawartego w katalogu vc f zastrzega sobie Jim Crafton. Kod ten jest rozpowszechniany z poniższą licencją. Copyright (c) 2000-2001. Jim Crafton A U rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES. INCLUDING. BUT NOT LIMITED TO. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT. INDIRECT. INCIDENTAL. SPECIAL. EXEMPLARY. OR CONSEQUENTIAL DAMAGES (INCLUDING. BUT NOT LIMITED TO. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES: LOSS OF USE. DATA. OR PROFITS: OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY. WHETHER IN CONTRACT. STRICT LIABILITY. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. NB: This software will not save the world.

394

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

D.12. X Window System Prawa autorskie do kodu źródłowego zawartego w katalogu XFree86-3.3 zastrzega sobie organizacja X Consortium. Kod ten jest rozpowszechniany z poniższą licencją. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use. copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS". WITHOUT WARRANTY OF ANY KINO. EXPRESS OR IMPLIED. INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM. DAMAGES OR OTHER LIABILITY. WHETHER IN AN ACTION OF CONTRACT. TORT OR OTHERWISE. ARISING FROM. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of the X Consortium shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the X Consortium.

Dodatek E

Porady dotyczące czytania kodu Rozdział 1. Wprowadzenie 1. Warto wyrobić w sobie nawyk czytania kodu wysokiej jakości pisanego przez innych. 2 . Kod należy czytać wybiórczo i w konkretnym celu. Czy chcemy nauczyć się

nowych wzorców, stylu kodowania, a może metod spełniania określonych wymagań? 3. Należy zwracać uwagę i uwzględniać określone niefunkcjonalne wymagania związane z kodem, które mogą spowodować powstanie pewnego stylu implementacji. 4. Pracując z istniejącym kodem, należy koordynować swoje wysiłki z jego autorami lub osobami odpowiedzialnymi za jego utrzymanie w celu uniknięcia duplikacji działań lub niezdrowych niedomówień. 5. Warto potraktować korzyści płynące z oprogramowania open-source jako swego rodzaju pożyczkę. Jest wówczas rzeczą oczywistą, że należy poszukać sposobów jej spłacenia poprzez przekazanie środowisku open-source czegoś od siebie. 6 . W wielu przypadkach jeżeli chcemy się dowiedzieć „w jaki sposób oni to

zrobili”, nie ma lepszego sposobu niż przyjrzenie się kodowi. 7. Szukając błędu, należy przeanalizować kod od objawów danego problemu po jego źródło. Nie należy badać części nie mających bezpośredniego związku z bieżącym problemem. 8 . W celu zlokalizowania błędu należy korzystać z debuggera, ostrzeżeń

kompilatora lub danych wyjściowych kodu symbolicznego, programu śledzącego wywołania w systemie, oferowanych przez bazę danych mechanizmów rejestrowania operacji języka SQL, narzędzi przechwytujących pakiety w sieci czy programów przechwytywania komunikatów systemów Windows.

396

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

9. Można z powodzeniem modyfikować duże, poprawnie zaprojektowane systemy przy jedynie minimalnej znajomości ich pełnej funkcjonalności. 1 0 . W przypadku dodawania nowych możliwości do systemu, pierwszym

zadaniem jest znalezienie implementacji podobnej funkcji, która zostanie wykorzystana jako szablon dla nowo tworzonej. 11. Aby przejść od specyfikacji funkcjonalności do implementacji kodu, należy prześledzić komunikaty lub wyszukać kod za pomocą słów kluczowych. 1 2 . Przenosząc kod do innego systemu lub modyfikując interfejsy, można

zaoszczędzić sobie wysiłku związanego z czytaniem kodu, skupiając uwagę na obszarach problematycznych, zidentyfikowanych przez kompilator. 13. W przypadku refaktoryzacji rozpoczynamy od działającego systemu i chcemy zapewnić, aby po zakończeniu działań również był on sprawny. Zestaw adekwatnych przypadków testowych pomaga w spełnieniu tego wymogu. 14. Czytając kod w celu wyszukania możliwości zastosowania refaktoryzacji, można maksymalizować osiągane korzyści rozpoczynając od architektury systemu i schodząc w dół, prowadząc poszukiwania na coraz wyższym poziomie szczegółowości. 15. Możliwość ponownego wykorzystania kodu jest kuszącą, ale złudną perspektywą. Należy ograniczyć swoje oczekiwania, by nie rozczarować się. 16. Jeżeli kod, który chce się ponownie wykorzystać, jest skomplikowany, trudny do zrozumienia i wyizolowania, należy raczej przyjrzeć się pakietom zapewniającym wyższy poziom abstrakcji lub poszukać innego kodu. 17. Analizując system programistyczny należy pamiętać, że składa się on z większej liczby elementów niż instrukcja wykonawcza. Trzeba zbadać strukturę plików i katalogów, proces kompilacji, konsolidacji i konfigurowania, interfejs użytkownika oraz dokumentację systemową. 18. Przeglądy oprogramowania powinny być traktowane jako szansa nauczenia czegoś siebie i innych, udzielenia pomocy komuś lub otrzymania jej dla siebie.

Rozdział 2. Podstawowe konstrukcje programistyczne 19. Badając program po raz pierwszy, warto zwykle rozpocząć od funkcji

ma i n .

20 . Sekwencję kaskadowych instrukcji

i f - e l s e i f - . . . - e l s e należy odczytywać jako wzajemnie wykluczające się możliwości wyboru.

21. Niekiedy uruchomienie programu może stanowić lepszy sposób na zrozumienie jego funkcjonalności niż przeczytanie jego kodu źródłowego. 2 2 . W przypadku badania niebanalnego programu przydatną rzeczą jest

zidentyfikowanie w pierwszej kolejności jego głównych części. 23. Należy zapoznać się z lokalnymi konwencjami nazewnictwa i korzystać z nich w celu odgadnięcia, do czego służą zmienne i funkcje.

Dodatek E ♦ Porady dotyczące czytania kodu

397

24. W przypadku modyfikowania kodu w oparciu o przypuszczenia, należy zaplanować proces, który pozwoli na zweryfikowanie początkowych hipotez. Może on być związany z wykonaniem sprawdzeń za pomocą kompilatora, wprowadzeniem asercji lub wykonaniem odpowiednich testów. 25. Zrozumienie jednej części kodu może pozwolić poznać inne fragmenty. 26. Należy rozwikływać skomplikowany kod, rozpoczynając od analizy jego prostych fragmentów. 27. Warto nabrać nawyku czytania dokumentacji elementów bibliotecznych, jakie się spotyka — pozwoli to podnieść zarówno umiejętności czytania, jak i pisania kodu. 28. Czytanie kodu to zadanie związane ze stosowaniem wielu alternatywnych strategii: analizy wstępującej lub zstępującej, użycia technik heurystycznych lub przeglądania komentarzy i zewnętrznej dokumentacji — wszystkie one powinny być wypróbowane, jeśli wymaga tego dany problem. 29. Pętle postaci for (i = 0 : i < n: i ++) są wykonywane n razy. Wszelkie zapisy innej postaci należy uważnie przeanalizować. 30. Wyrażenia porównania zawierające koniunkcję dwóch nierówności z jednym identycznym składnikiem należy traktować jako sprawdzenie przynależności do przedziału. 31. Często można zrozumieć znaczenie wyrażenia dzięki zastosowaniu go względem przykładowych danych. 32. Skomplikowane wyrażenia logiczne należy upraszczać, stosując prawa De Morgana. 33. Odczytując koniunkcję, zawsze można przyjąć, że wyrażenia znajdujące się po lewej stronie badanego wyrażenia mają wartość prawdy. Odczytując alternatywę, można podobnie przyjąć, że wyrażenia znajdujące się po lewej stronie badanego wyrażenia mają wartość fałszu. 34. Należy dokonywać reorganizacji kodu, nad którym ma się kontrolę, w celu zwiększenia jego czytelności. 35. Wyrażenia zawierające operator ? należy odczytywać podobnie do kodu instrukcji if. 36. Nie ma żadnego powodu, aby poświęcać czytelność kodu na rzecz wydajności. 37. Choć jest prawdą, że wydajne algorytmy i pewne techniki optymalizacji mogą komplikować kod, a przez to utrudnić jego analizę, nie oznacza to, że zapewnienie zwięzłości i wysokiego stopnia skomplikowania kodu zapewni zwiększenie jego wydajności. 38. Kreatywne rozmieszczenie kodu może czasem być wykorzystane do zwiększenia jego czytelności. 39. Wyrażenie można uczynić bardziej czytelnym poprzez dodanie białych znaków, podzielenie go na mniejsze części przy użyciu zmiennych tymczasowych lub użycie nawiasów w celu podkreślenia kolejności wykonywania określonych działań.

398

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

40. Czytając kod znajdujący się pod naszą kontrolą, warto nabrać nawyku dodawania komentarzy tam, gdzie to konieczne. 41. Można poprawić czytelność słabo zapisanego kodu dzięki lepszej organizacji wcięć i przemyślanemu doborowi nazw zmiennych. 42. Kiedy poznaje się historię wersji programu, która zawiera wykonanie globalnej zmiany schematu wcięć, przy użyciu programu dijf, często można uniknąć problemów związanych z wprowadzonymi zmianami dzięki określeniu opcji -w. nakazującej programowi ¿//^ignorowanie różnic pod względem białych znaków. 43. Treść pętli do jest wykonywana co najmniej raz. 44. Wykonując operacje arytmetyczne, należy odczytywać zapis a & b jako a % (b + 1 ), kiedy b = 2 " - 1 . 45. Zapis a « n należy odczytywać jako a * k, gdzie k = 2". 46. Zapis a » n należy odczytywać jako a / k, gdzież = 2". 47. Należy badać tylko jedną strukturę sterującą na raz i traktować jej treść jak czarną skrzynkę. 48. Wyrażenie kontrolne każdej struktury sterującej należy traktować jako asercję dla kodu, który zawiera. 49. Instrukcje return, goto, break oraz conti nue, jak również obsługa wyjątków, kolidują ze strukturalnym przebiegiem wykonania. Ich zachowanie należy analizować oddzielnie, gdyż wszystkie one zwykle albo kończą albo ponawiają wykonywanie przetwarzanej pętli. 50. Przydatna abstrakcja dotycząca analizy właściwości pętli jest oparta na pojęciach zmienników oraz niezmienników. 51. Analizę kodu można uprościć poprzez jego reorganizację, korzystając z przekształceń zachowujących znaczenie.

Rozdział 3. Zaawansowane typy danych języka C 52. Rozpoznając funkcje pełnione przez określone konstrukcje językowe można lepiej zrozumieć kod, który z nich korzysta. 53. Należy rozpoznawać i klasyfikować powody używania wskaźników. 54. Wskaźniki są używane w języku C w celu tworzenia powiązanych struktur danych, odwoływania się do dynamicznie przydzielanych struktur danych, implementacji wywołań przez referencje, uzyskiwania dostępu do elementów danych i ¡terowania po nich, przekazywania tablic jako argumentów, odwoływania się do funkcji, jako aliasy innych wartości, w celu reprezentowania ciągów znaków, w celu zapewnienia bezpośredniego dostępu do pamięci systemowej. 55. Argumenty funkcji przekazywane przez referencję są wykorzystywane w celu zwracania wyników funkcji lub w celu uniknięcia narzutu czasowego związanego z kopiowaniem argumentu funkcji.

r Dodatek E ♦ Porady dotyczące czytania kodu

399

56. Wskaźnik na adres elementu tablicy może być wykorzystany do uzyskania dostępu do elementu znajdującego się pod określonym indeksem. 57. Arytmetykę na wskaźnikach na elementy tablicy charakteryzują te same cechy semantyczne, co arytmetykę odpowiednich indeksów tablicy. 58. Funkcje wykorzystujące globalne lub statyczne zmienne lokalne nie są w większości przypadków wielobieżne. 59. Wskaźniki znakowe różnią się od tablic znaków. 60. Należy rozpoznawać i klasyfikować powody użycia struktury lub unii w każdym przypadku. 61. Struktury są używane w programach pisanych w C w celu: zgrupowania elementów danych używanych zwykle wspólnie; zwrócenia wielu elementów danych z funkcji; konstruowania powiązanych struktur danych; odwzorowania organizacji danych w urządzeniach sprzętowych, łączach sieciowych oraz nośnikach danych; implementacji abstrakcyjnych typów danych oraz tworzenia programów zgodnie z paradygmatem obiektowym. 62. Unie są używane w języku C w celu zapewnienia wydajnego wykorzystania pamięci, zaimplementowania polimorfizmu oraz umożliwienia dostępu do danych przy użyciu różnych reprezentacji wewnętrznych. 63. Wskaźnik zainicjalizowany tak, aby wskazywał na obszar przechowywania N elementów, może być rozwikływany tak, jakby to była tablica N elementów. 64. Dynamicznie przydzielane bloki pamięci są zwalniane bezpośrednio w momencie zakończenia programu lub przez mechanizm czyszczenia pamięci; bloki pamięci przydzielane na stosie są zwalniane, kiedy następuje wyjście z funkcji, w której zostały przydzielone. 65. Programy pisane w języku C używają deklaracji ty p e d e f w celu zapewnienia obsługi abstrakcji, zwiększenia czytelności kodu, zapobieżenia problemom związanym z przenośnością oraz emulowania mechanizmu deklaracji klas znanego z języków C++ oraz Java. 66 . Deklaracje typedef można odczytywać tak, jakby były definicjami zmiennych:

nazwa definiowanej zmiennej to nazwa typu: typ zmiennej to typ odpowiadający tej nazwie.

Rozdział 4. Struktury danych języka C 67. Jawnie definiowane operacje wykonywane na strukturach danych należy odczytywać w kontekście odpowiednich abstrakcyjnych klas danych. 68 . Wektory są zwykle realizowane w kodzie języka C przy użyciu wewnętrznego

typu tablicowego bez podejmowania próby oddzielenia właściwości wektora od odpowiedniej implementacji. 69. Tablica licząca N elementów jest w całości przetwarzana w przypadku użycia zapisu for (i » 0: i < N: i ++). Wszelkie inne zapisy powinny wzmóc czujność czytającego kod.

400

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

70. Wyrażenie sizeof(x) zawsze daje w wyniku poprawną liczbę bajtów dla przetworzenia tablicy x (ale nie wskaźnika) za pomocą funkcji memset lub memcpy. 71. Zakresy są zwykle reprezentowane przy użyciu swojego pierwszego elementu oraz pierwszego elementu znajdującego się poza nimi. 72. Liczba elementów należących do zakresu asymetrycznego jest równa różnicy między kresem górnym a dolnym. 73. Kiedy kres górny zakresu asymetrycznego jest równy kresowi dolnemu, zakres jest pusty. 74. Kres dolny zakresu asymetrycznego reprezentuje pierwszy zajęty element; kres górny — pierwszy wolny element. 75. Tablice struktur często reprezentują tabele składające się z rekordów i pól. 76. Wskaźniki na struktury często reprezentują kursor służący do uzyskiwania dostępu do odpowiednich rekordów i pól. 77. Macierze przydzielane dynamicznie są przechowywane jako wskaźniki na kolumny tablicy lub jako wskaźniki na wskaźniki na elementy. W obu przypadkach dostęp odbywa się podobnie jak w przypadku tablic dwuwymiarowych. 78. Macierze przydzielane dynamicznie, przechowywane jako zwykłe tablice, adresują swoje elementy przy użyciu odpowiednich funkcji dostępowych. 79. Abstrakcyjny typ danych stanowi gwarancję jednolitego używania odpowiednich elementów implementacji. 80. Tablice są używane w celu organizacji tablic przeglądowych indeksowanych kolejnymi nieujemnymi liczbami całkowitymi. 81. Użycie tablic pozwala często na wydajne kodowanie struktur sterujących, co upraszcza logikę programu. 82. Tablice są używane do kojarzenia danych z kodem poprzez przechowywanie na każdej pozycji elementu danych oraz wskaźnika na funkcję przetwarzania elementu. 83. Tablice umożliwiają sterowanie działaniem programu poprzez przechowywanie danych lub kodu używanego przez maszyny abstrakcyjne lub wirtualne zaimplementowane w programie. 84. Wyrażenie postaci sizeof(x)/sizeof(x[0]) należy odczytywać jako liczbę elementów tablicy x. 85. Struktura zawierająca element o nazwie jednokierunkowej.

n e xt

zwykle definiuje węzeł listy

86 . Stały wskaźnik (na przykład globalny, statyczny lub przydzielony na stercie)

na węzeł listy często reprezentuje jej głowę. 87. Struktura zawierająca wskaźniki dwukierunkowej.

p re v

oraz n ext zwykle definiuje węzeł listy

Dodatek E ♦ Porady dotyczące czytania kodu

401

88. Trudności związane ze zrozumieniem działań na wskaźnikach w przypadku złożonych struktur danych można pokonać, rysując prostokąty reprezentujące elementy oraz strzałki reprezentujące wskaźniki. 89. Rekurencyjne struktury danych są często przetwarzane za pomocą algorytmów rekurencyjnych. 90. Niebanalne algorytmy manipulacji strukturami danych są zwykle parametryzowane poprzez użycie jako argumentu funkcji lub szablonu. 91. Wierzchołki grafu są przechowywane sekwencyjnie w tablicach, listach lub poprzez krawędzie grafu. 92. Krawędzie grafu są zwykle reprezentowane albo pośrednio poprzez wskaźniki, albo bezpośrednio jako odrębne struktury. 93. Krawędzie grafu są często przechowywane jako dynamicznie przydzielane tablice lub listy połączone z wierzchołkami grafu. 94. W grafie nieskierowanym reprezentacja danych powinna traktować pary połączonych wierzchołków jako równoważne, a kod przetwarzania podobnie powinien nie wyróżniać żadnego z wierzchołków pod względem kierunku. 95. W przypadku grafów niespójnych, kod przechodzenia powinien zostać zapisany tak, aby uwzględniał odizolowane podgrafy. 96. W przypadku grafów zawierających cykle, kod przechodzenia powinien być zapisany tak, aby można było uniknąć wpadnięcia w pętlę w trakcie pokonywania cyklu. 97. W ramach złożonych struktur grafowych mogą ukrywać się inne, odrębne struktury.

Rozdział 5. Zaawansowane techniki sterowania przebiegiem programów 98. Algorytmy i struktury danych zdefiniowane rekurencyjnie są często implementowane poprzez użycie definicji funkcji rekurencyjnych. 99. Analizę funkcji rekurencyjnej należy rozpocząć od warunku początkowego i skonstruować nieformalny dowód tego, że każdy krok rekurencyjny zbliża jej wykonanie do nierekurencyjnego warunku początkowego. 100. Proste języki są często poddawane analizie składniowej przy użyciu szeregu funkcji, które są zgodne ze strukturą gramatyki języka. 1 0 1 . Funkcje wzajemnie wykluczające należy analizować, biorąc pod uwagę

rekurencyjną definicję odpowiednich pojęć, na których są oparte. 102. Wywołania rekurencji ogonowej są równoważne powrotowi pętli do wykonywania funkcji od początku. 103. Można z łatwością zlokalizować metody, które mogą powodować pośrednio generowane wyjątki poprzez uruchomienie kompilatora języka Java względem kodu źródłowego klasy po usunięciu klauzuli throws z definicji metody.

402

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

104. Struktura kodu wykorzystującego maszyny wieloprocesorowe jest często projektowana z uwzględnieniem właściwości procesów i wątków. 105. Zespołowy model równoległy jest stosowany w celu rozłożenia pracy pomiędzy procesory lub utworzenia zestawu zadań używanych do przydzielania standardowych części wykonywanej pracy. 106. Model równoległy zwierzchnika-podwładnego bazujący na wątkach jest zwykle stosowany w celu zachowania krótkiego czasu odpowiedzi zadania głównego poprzez przekazanie wykonania kosztownych lub blokujących operacji podzadaniom. 107. Model równoległy zwierzchnika-podwładnego bazujący na procesach jest zwykle stosowany w celu zapewnienia ponownego użycia istniejących programów lub utworzenia struktury odizolowanych modułów systemu posiadających dobrze zdefiniowane interfejsy. 108. W przypadku potokowego modelu równoległego każde zadanie otrzymuje pewne dane wejściowe, wykonuje na nich pewne działania i przekazuje dane wynikowe do kolejnego zadania w celu przeprowadzenia innych działań. 109. Wyścigi czasem niełatwo wykryć i kod prowadzący do ich powstawania rozkłada się na wiele funkcji lub modułów. Stąd też podobne problemy trudno jest wyizolować. 1 1 0 . Należy z ogromną ostrożnością badać kod manipulujący strukturami danych

i wywołania biblioteczne, które występują w funkcji obsługi sygnału. 111. Czytając kod zawierający makra należy pamiętać, że nie są one ani funkcjami, ani instrukcjami. 1 1 2 . Makrodefinicje zdefiniowane wewnątrz bloku do { ...} while (0)

są równoważne instrukcjom znajdującym się wewnątrz bloku. 113. Makro umożliwia uzyskiwanie dostępu do zmiennych lokalnych dostępnych w miejscu ich wywołania. 114. Wywołania makr mogą zmieniać wartości ich argumentów. 115. Łączenie znaczników w ramach makrodefinicji umożliwia tworzenie nowych identyfikatorów.

Rozdział 6. Metody analizy dużych projektów 116. Organizację projektu można zbadać, przeglądając jego drzewo kodu źródłowego — hierarchiczną strukturę katalogów zawierających kod źródłowy projektu. Drzewo kodu źródłowego często odzwierciedla architekturę projektu oraz strukturę procesu programistycznego. 117. Drzewo kodu źródłowego aplikacji często odzwierciedla strukturę jej wdrożenia. 118. Nie należy bać się dużych zbiorów kodu źródłowego — zwykle są one lepiej zorganizowane niż ma to miejsce w przypadku mniejszych, doraźnych projektów.

Dodatek E ♦ Porady dotyczące czytania kodu

403

119. Pracując nad dużym projektem pierwszy raz, należy poświęcić nieco czasu zapoznaniu się ze strukturąjego drzewa katalogów. 1 2 0 . „Kod źródłowy” projektu to o wiele więcej niż tylko instrukcje języka

komputerowego, które są kompilowane w celu otrzymania programu wykonywalnego. Drzewo kodu źródłowego projektu zwykle zawiera również specyfikacje, dokumentację użytkową i systemową, skrypty testowe, zasoby multimedialne, narzędzia służące do przeprowadzania procesu budowy elementów systemu, przykłady, pliki lokalizacyjne, pliki kontroli wersji, procedury instalacyjne oraz informacje licencyjne. 1 2 1 . Proces budowy dużego projektu jest zwykle określany w sposób deklaratywny

przy użyciu zależności. Zależności są zamieniane na konkretne działania budowania wykonywane przez narzędzia, takie jak program make i jego odmiany. 1 2 2 . Pliki makefile dużych projektów są często generowane dynamicznie

po przeprowadzeniu etapu konfiguracji — w celu zbadania pliku makefile należy wykonać działania konfiguracyjne związane z projektem. 123. Działanie programu make można przetestować korzystając z opcji -n, która pozwala zapoznać się z etapami złożonego procesu budowy. 124. System kontroli wersji oferuje sposób otrzymywania aktualnych wersji kodu źródłowego ze swojego repozytorium. 125. Poleceń wyświetlających słowa kluczowe identyfikacji wersji plików wykonywalnych można używać w celu ich dopasowania do odpowiedniego kodu źródłowego. 126. Wykorzystując numer kodowy błędu, można zlokalizować określony element w bazie danych śledzenia błędów. 127. Repozytorium systemu kontroli wersji można użyć w celu zidentyfikowania sposobu implementacji określonych zmian. 128. Niestandardowe narzędzia są używane w wielu różnych kontekstach procesu rozwoju aplikacji, w tym konfiguracji, zarządzaniu procesem budowy, generowaniu kodu, testowaniu oraz tworzeniu dokumentacji. 129. Diagnostyczne dane wyjściowe programu mogą pomóc w zrozumieniu najważniejszych części przebiegu sterowania programu oraz elementów danych. 130. Miejsca, w których znajduje się instrukcje śledzące, zazwyczaj określają istotne fragmenty funkcjonowania algorytmu. 131. Asercje są wykorzystywane do weryfikacji etapów działania algorytmów, parametrów pobieranych przez funkcję, przebiegu sterowania programu, właściwości sprzętu oraz wyników przypadków testowych. 132. Asercji weryfikacji algorytmu można używać w celu potwierdzenia swojego zrozumienia jego działania lub jako punktu rozpoczęcia analiz. 133. Argument funkcji oraz wynikowe asercje często dokumentują warunki wstępne oraz warunki końcowe funkcji. 134. Asercji, które testują całe funkcje, można używać jako instrukcji specyfikacji dla każdej danej funkcji.

404

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

135. Przypadki testowe mogą częściowo zastępować specyfikacje funkcjonalne. 136. Dane wejściowe przypadku testowego można wykorzystać w celu „ręcznego” wykonania fragmentów kodu źródłowego.

Rozdział 7. Standardy i konwencje zapisu kodu 137. Znając organizację pliku wspólną dla danej bazy kodu źródłowego, można w wydajny sposób go przeszukiwać. 138. Należy zapewnić, aby ustawienia tabulatorów używanego edytora lub drukarki odpowiadały specyfikacjom wskazówek stylistycznych mających zastosowanie w przypadku czytanego kodu. 139. Wcięć bloków kodu można używać w celu szybkiego wstępnego zapoznania się z jego strukturą. 140. Niekonsekwentnie sformatowany kod powinien natychmiast wzmóc czujność osoby go czytającej. 141. Należy zwracać szczególną uwagę na fragmenty kodu opatrzone sekwencjami XXX, FIXME oraz TODO, mogą bowiem zawierać błędy. 142. Stałe są nazywane przy użyciu wielkich liter, zaś słowa są rozdzielane znakiem podkreślenia. 143. W programach zgodnych z konwencjami zapisu kodu języka Java nazwy pakietów zawsze rozpoczynają się od najwyższego poziomu nazwy domeny (na przykład org., com.. sun.), nazwy klas i interfejsów rozpoczynają się od wielkiej litery, zaś metod i zmiennych od małej. 144. Przedrostki znaczników typu używane w przypadku notacji węgierskiej przed nazwą formantu interfejsu użytkownika pomagają w określeniu pełnionej przez niego roli. 145. Różne standardy programowania mogą sugerować niezgodne ze sobą pojęcia tego, co należy uważać za konstrukcję przenośną. 146. Badając kod pod względem kwestii przenośności i posiłkując się określonym standardem zapisu kodu, należy poznać zakres i ograniczenia wymagań standardu w odniesieniu do przenośności. 147. Kiedy funkcjonalność graficznego interfejsu użytkownika zostanie zaimplementowana przy użyciu odpowiednich konstrukcji programistycznych, poprawność przyjęcia specyfikacji danego interfejsu może zostać w banalny sposób zweryfikowana poprzez zwykłe przejrzenie kodu. 148. Poznanie organizacji i mechanizmów automatyzacji procesu konsolidacji projektu umożliwia szybkie odczytywanie i rozumienie odpowiednich reguł budowania projektu. 149. Badając proces publikacji systemu, jako podstawę często można wykorzystać wymagania odpowiedniego formatu dystrybucyjnego.

Dodatek E ♦ Porady dotyczące czytania kodu

405

Rozdział 8. Dokumentacja 150. Wysiłki związane z czytaniem kodu należy wspierać wszelkiego rodzaju dostępną dokumentacją. 151. Minuta lektury dokumentacji może zaoszczędzić godzinę czytania kodu. 152. Dokumentu specyfikacji systemu należy używać w celu zrozumienia cech środowiska, w którym będzie funkcjonował czytany kod. 153. Specyfikacji wymagań programowych należy używać jako punktu odniesienia dla lektury i oceny kodu. 154. Specyfikację projektową należy wykorzystywać jako przewodnik po strukturze kodu oraz jego poszczególnych elementach. 155. Dokument specyfikacji testowej zapewnia dostęp do danych, których można użyć w celu sprawdzenia „na sucho” czytanego kodu. 156. W przypadku nieznanego systemu, opis funkcjonalny oraz podręcznik użytkownika mogą zapewnić dostęp do istotnych informacji, pomagających w lepszym zrozumieniu kontekstu czytanego kodu. 157. Podręcznika użytkownika można użyć w celu szybkiego znalezienia dodatkowych informacji dotyczących komponentów kodu związanych z logiką warstwy prezentacji i aplikacji, zaś podręcznik administratora pozwala znaleźć szczegóły dotyczące interfejsów, formatów plików oraz komunikatów 0 błędach, jakie można spotkać w kodzie. 158. Dokumentacja stanowi szybki sposób poznania ogólnego obrazu systemu 1zrozumienia kodu, który jest związany z określoną funkcjonalnością. 159. Dokumentacja często odzwierciedla, a przez to ujawnia, strukturę systemu. 160. Dokumentacja pomaga w zrozumieniu skomplikowanych algorytmów i struktur danych. 161. Tekstowy opis algorytmu może sprawić, że zupełnie nieczytelny fragment kodu stanie się możliwy do zrozumienia. 162. Dokumentacja często pozwala odkryć znaczenie identyfikatorów występujących w kodzie źródłowym. 163. Dokumentacja może nieść ze sobą uzasadnienie wymagań niefunkcjonalnych. 164. Dokumentacja wyjaśnia wewnętrzne interfejsy programistyczne. 165. Ze względu na fakt, że dokumentacja bywa rzadko testowana i ogólnie jest pod tym względem traktowana z mniejszą uwagą niż faktyczny kod programu, często bywa błędna, niepełna lub nieaktualna. 166. Dokumentacja oferuje przypadki testowe i przykłady faktycznego użycia aplikacji. 167. Dokumentacja często opisuje znane problemy i błędy implementacyjne. 168. Znane wady środowisk są zazwyczaj dokumentowane w kodzie źródłowym.

406

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

169. Dokumentacja zmian może pomóc w lokalizowaniu problemów. 170. Częste lub sprzeczne zmiany wprowadzane w tych samych partiach kodu źródłowego często wskazują na występowanie fundamentalnych błędów projektowych, które osoby konserwujące kod starają się naprawić szeregiem poprawek. 171. Podobne poprawki zastosowane względem różnych części kodu źródłowego wskazują na występowanie łatwego do popełnienia błędu lub przeoczenia, które z dużą dozą prawdopodobieństwa mogą występować również w innych miejscach. 172. Dokumentacja często może dawać błędne wyobrażenie o kodzie źródłowym. 173. Należy być świadomym możliwości występowania nieudokumentowanych funkcji. Każdy taki przypadek należy zaklasyfikować jako uzasadniony, wynikający z nieuwagi lub o wrogim charakterze i zgodnie z tym zdecydować czy kod lub dokumentacja powinny zostać poprawione. 174. Niekiedy dokumentacja nie opisuje systemu zgodnie zjego faktyczną implementacją, lecz według kryteriów, które powinny być lub dopiero będą zaimplementowane. 175. W dokumentacji kodu źródłowego wyraz grok oznacza zwykle „rozumieć”. 176. W razie napotkania problemów ze zrozumieniem dokumentacji zawierającej nieznane wyrazy, można spróbować odszukać je w słowniczku dokumentacji (o ile taki istnieje), w pozycji The New Hacker’s Dictionary [Ray96] lub za pomocą wyszukiwarki internetowej. 177. Szukając dokumentacji kodu, warto wziąć pod uwagę również mniej popularne źródła, takie jak komentarze, normy, publikacje, przypadki testowe, listy dyskusyjne, grupy dyskusyjne, rejestry zmian, bazy danych zagadnień, materiały marketingowe oraz sam kod źródłowy. 178. Dokumentację zawsze należy traktować z dozą krytycyzmu. Ze względu na fakt, że dokumentacja nigdy nie jest wykonywana i rzadko bywa testowana albo formalnie korygowana w takim samym stopniu co kod, często może okazać się myląca lub po prostu błędna. 179. Kod można odczytać jako specyfikację tego, co powinien robić, a nie tego, co pozornie próbuje robić. 180. Czytając dokumentację dużego systemu, należy zapoznać się z jej ogólną strukturą i stosowanymi konwencjami. 181. Spotkawszy się z obszerną dokumentacją, można zwiększyć efektywność własnej lektury dzięki zastosowaniu odpowiednich narzędzi lub przesłaniu tekstu do urządzenia wyjściowego oferującego wysoką jakość, takiego jak drukarka laserowa.

* Dodatek E ♦ Porady dotyczące czytania kodu

407

Rozdział 9. Architektura 182. Jeden system może (i jest tak zawsze w niebanalnych przypadkach) jednocześnie wykazywać cechy wielu stylów. Różne style architektoniczne mogą się ujawniać w przypadku spojrzenia na system z różnych perspektyw, w wyniku zbadania jego różnych części lub zastosowania różnych poziomów dekompozycji. 183. Architektura scentralizowanego repozytorium jest stosowana w aplikacjach skupiających całą rzeszę użytkowników oraz w sytuacji, gdy kilka częściowo autonomicznych procesów musi współpracować w celu uzyskania dostępu do współdzielonych informacji lub zasobów. 184. System tablicowy wykorzystuje scentralizowane repozytorium w słabo ustrukturyzowanych danych w postaci par klucz-wartość jako węzła komunikacji dla wielu różnych elementów kodu. 185. Kiedy przetwarzanie można modelować, projektować i implementować jako serię przekształceń danych, często używa się architektury przepływu danych (potoków i filtrów). 186. Architektura przepływu danych jest często stosowana w środowiskach automatycznego, wsadowego przetwarzania danych, szczególnie na platformach, które w wydajny sposób obsługują narzędzia przekształcania danych. 187. Sygnałem wskazującym na wykorzystanie architektury przepływu danych jest wykorzystanie plików tymczasowych lub potoków w celu komunikowania się między różnymi procesami. 188. Należy używać diagramów w celu modelowania związków istniejących między klasami w architekturach obiektowych. 189. W celu dokonania inżynierii wstecznej architektury systemu można zaimportować jego kod źródłowy do narzędzia modelującego. 190. Systemy posiadające wiele alternatywnych równorzędnych podsystemów są często organizowane według architektury warstwowej. 191. Architektury warstwowe są zwykle implementowane poprzez zestawienie komponentów programistycznych ze standardowymi interfejsami. 192. Dana warstwa systemu widzi warstwy niższe jako abstrakcje i (o ile spełnia stawiane wymagania) nie musi się martwić o sposoby swojego użycia przez warstwy wyższe. 193. Interfejs warstwy może składać się albo z rodziny komplementarnych funkcji obsługujących określone pojęcie, albo z serii wymiennych funkcji obsługujących różne implementacje abstrakcyjnego interfejsu. 194. Systemy implementowane w języku C często wyrażają operacje multipleksujące interfejsu warstwowego przy użyciu tablic wskaźników na funkcje. 195. Systemy zaimplementowane w językach obiektowych bezpośrednio wyrażają operacje związane z multipleksacją interfejsów warstw poprzez użycie wywołań metod wirtualnych.

408

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

196. System może być zorganizowany wzdłuż wielu osi poprzez użycie różnych odrębnych modeli dekompozycji hierarchicznej. 197. Technika przecinania programów pozwala zebrać razem dane oraz zależności sterujące danym programem. 198. W systemach współbieżnych pojedynczy komponent systemu funkcjonuje jako centralny menedżer, który uruchamia, zatrzymuje oraz koordynuje wykonanie innych procesów i zadań systemowych. 199. Wiele rzeczywistych systemów zawiera najlepsze elementy kilku architektur. W takim przypadku nie warto na próżno szukać ogólnego obrazu architektury — należy lokalizować, rozpoznawać i analizować wszystkie style architektoniczne jako oddzielne, aczkolwiek powiązane ze sobą struktury. 200 . Diagram przejść stanów często pomaga w rozwikłaniu sposobu działania

maszyny stanów. 2 0 1 . W przypadku dużego bloku kodu. istotną rzeczą jest zrozumienie mechanizmów

umożliwiających dokonanie jego dekompozycji na oddzielne jednostki. 20 2 . Fizycznym ograniczeniem modułu jest w większości przypadków

pojedynczy plik, pliki zorganizowane w ramach katalogu lub kolekcja plików o unikatowym przedrostku. 203. Moduły w języku C często składają się z pliku nagłówkowego zapewniającego dostęp do publicznego interfejsu oraz pliku źródłowego języka C oferującego odpowiednią implementację. 204. Konstruktory obiektów są często używane w celu przydzielenia zasobów związanych z obiektem oraz zainicjalizowania jego stanu. Z kolei destruktory są zwykle wykorzystywane do zwalniania zasobów zajętych przez obiekt w czasie jego istnienia. 205. Metody obiektu często wykorzystują pola klasy w celu przechowywania danych, które sterują działaniem wszystkich metod (takich jak tabela przeglądowa lub słownik) lub w celu zachowania informacji o stanie związanym z działaniem klasy (na przykład licznik służący do przypisywania unikatowych identyfikatorów każdemu obiektowi. 206. W przypadku dobrze zaprojektowanych klas, wszystkie pola są deklarowane jako prywatne i dostęp do nich odbywa się poprzez publiczne metody dostępu. 207. Napotkawszy deklarację friend, należy się zastanowić nad uzasadnieniem powodu przesłonięcia mechanizmu enkapsulacji klasy. 208. Przeciążanie operatorów jest używane sporadycznie w celu zwiększenia użyteczności określonej klasy i znacznie częściej w celu wyposażenia klasy implementującej wielkość liczbową w pełną funkcjonalność związaną z wbudowanymi typami arytmetycznymi. 209. Implementacje ogólne są realizowane albo w czasie kompilacji poprzez podstawianie makr i mechanizmy oferowane przez język, takie jak szablony w języku C++ oraz pakiety ogólne języka Ada, albo w czasie uruchomienia poprzez użycie wskaźników na elementy i funkcje lub polimorfizmu obiektów.

Dodatek E ♦ Porady dotyczące czytania kodu

409

2 1 0 . Abstrakcyjne typy danych są często używane w celu zapewnienia enkapsulacji

często wykorzystywanych schematów organizacji danych (takich jak drzewa, listy lub stosy) lub w celu ukrycia szczegółów implementacyjnych typu danych przed użytkownikiem. 211. Biblioteki są używane z wielu różnych powodów: w celu zapewnienia możliwości wielokrotnego użycia kodu źródłowego i obiektowego, w celu zapewnienia odpowiedniej organizacji kolekcji modułów, określenia struktury i zoptymalizowania procesu konsolidacji oraz w celu zapewnienia możliwości ładowania funkcji aplikacji na żądanie. 2 1 2 . Duże i rozproszone systemy są często implementowane w formie wielu

współpracujących procesów. 213. W wielu przypadkach strukturę tekstowego repozytorium danych można rozszyfrować, badając przechowywane w nim dane. 214. Schemat relacyjnej bazy danych można zbadać, wykonując zapytania na tabelach słownika danych lub używając określonych poleceń SQL charakterystycznych dla danego systemu, takich jak SHOW TABLE. 215. Po rozpoznaniu użytego ponownie elementu architektonicznego należy sprawdzić jego oryginalny opis w celu uzyskania dodatkowych informacji odnośnie do sposobu jego użycia. 216. Przy dokładnym badaniu aplikacji utworzonej na podbudowie schematu strukturalnego najlepszym rozwiązaniem jest rozpoczęcie od zbadania samego schematu. 217. Należy ograniczyć swoje oczekiwania w przypadku lektury kodu wygenerowanego przez kreator, aby nie być zawiedzionym. 218. Poznawszy kilka podstawowych wzorców projektowych można odkryć, że nasze spojrzenie na kod architektury systemu ulega zmianie: zdolności postrzegania i słownictwo ulegają rozszerzeniu, pozwalając rozpoznawać i opisywać wiele często używanych form. 219. Często spotyka się powszechnie stosowane wzorce bez jawnych odwołań do ich nazw, gdyż ponowne użycie projektów architektonicznych często poprzedza ich opis w postaci wzorca. 2 2 0 . Należy starać się zrozumieć architekturę w kontekście odpowiednich

wzorców, nawet jeśli nie ma o nich bezpośrednio mowy w kodzie. 2 2 1 . Większość interpreterów działa zgodnie z architekturą utworzoną na podbudowie

maszyny stanów, której działania są uzależnione od bieżącego stanu interpretera, instrukcji programu oraz stanu programu. 2 2 2 . W wielu przypadkach architektury referencyjne określają strukturę notacyjną

w obszarze zainteresowania, która nie zawsze jest przestrzegana przez konkretne implementacje.

410

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

Rozdział 10. Narzędzia pomocne w czytaniu kodu 223. Używając narzędzi leksykalnych, można efektywnie wyszukiwać określone wzorce w dużym pliku źródłowym lub w wielu plikach. 224. Edytora kodu oraz poleceń wyszukiwania w formie wyrażeń regularnych można używać w celu przeglądania obszernych plików źródłowych. 225. Przeglądając pliki źródłowe, należy korzystać z edytora w trybie tylko do odczytu. 226. Definicję funkcji można zlokalizować, używając wyrażenia regularnego typu *nazwa funkcji. 227. Klasy znaków wyrażeń regularnych służą do wyszukiwania zmiennych o nazwach pasujących do pewnego wzorca. 228. Klasy znaków z negacją mogą być używane w celu uniknięcia fałszywych dopasowań. 229. Symbole występujące razem w tym samym wierszu można wyszukiwać przy użyciu wyrażenia regularnego symbol-1.* symbol-2. 230. Należy korzystać z mechanizmu znaczników (ang. tags) oferowanego przez edytor w celu szybkiego lokalizowania definicji elementów. 231. Funkcjonalność mechanizmu przeglądania edytora można rozszerzyć za pomocą specjalizowanego narzędzia służącego do tworzenia znaczników. 232. Przeglądając duże pliki źródłowe, można zapoznać się z widokiem kodu „z lotu ptaka” korzystając z opcji podglądu edytora. 233. Edytora można używać w celu wykrywania par nawiasów okrągłych, kwadratowych i klamrowych. 234. Wzorców kodu występujących w wielu plikach można szukać przy użyciu programu grep. 235. Przy użyciu programu grep można lokalizować deklaracje, definicje oraz miejsca użycia symboli. 236. Kiedy nie jest się pewnym, czego dokładnie się szuka, w kodzie źródłowym programu należy wyszukać rdzeń słowa kluczowego. 237. Dane wyjściowe innych narzędzi można przekazywać potokiem do programu grep w celu wyizolowania poszukiwanych elementów. 238. Dane wyjściowe programu grep można przekazywać potokiem do innych narzędzi w celu zautomatyzowania wykonania pewnych wyrafinowanych zadań. 239. Wyniki przeszukiwania kodu można wykorzystywać wielokrotnie poprzez edycję strumieniową danych wyjściowych programu grep. 240. Niepotrzebne dane wyjściowe programu grep można odfiltrować, wybierając spośród nich tylko te, które nie pasują do określonego wzorca szumu (opcja grep -v).

Dodatek E ♦ Porady dotyczące czytania kodu

411

241. Używając narzędziafgrep, można dopasowywać kod źródłowy do list ciągów znaków. 242. Przeszukanie wszystkich komentarzy i kodu języków o identyfikatorach niezależnych od wielkości liter (na przykład Basic) jest możliwe przy użyciu wzorca dopasowania bez uwzględnienia wielkości liter (grep -i). 243. Używając przełącznika -n, można utworzyć listę plików oraz numerów wierszy, które odpowiadają danemu wyrażeniu regularnemu. 244. W celu porównania różnych wersji pliku lub programu można użyć narzędzia diff. 245. Uruchamiając program diff, można użyć przełącznika -b w celu określenia, że algorytm porównywania plików ma ignorować końcowe odstępy, opcji -w w celu ignorowania wszystkich różnic na poziomie białych znaków oraz opcji -i w celu określenia braku wrażliwości na wielkość liter. 246. Nie należy mieć żadnych obiekcji przed tworzeniu własnych narzędzi pomocnych w czytaniu kodu. 247. Opracowując własne narzędzia pomocne w czytaniu kodu: należy wykorzystywać możliwości współczesnych języków oferujących mechanizm szybkiego modelowania, rozpoczynać od prostego projektu, stopniowo zwiększając jego funkcjonalność, używać technik heurystycznych wykorzystujących strukturę leksykalną kodu, można dopuścić występowanie pewnej ilości „szumów” lub „ciszy” oraz należy używać narzędzi w celu wstępnego przetworzenia danych wejściowych lub końcowego przetworzenia danych wyjściowych. 248. Czytając kod warto korzystać z kompilatora i określić odpowiedni poziom ostrzeżeń oraz uważnie badać wyniki. 249. Preprocesora C można używać w celu zrozumienia działania programów, które go nadużywają. 250. W celu pełnego zrozumienia, w jaki sposób kompilator postępuje z określonym fragmentem kodu, można zapoznać się z wygenerowanym kodem symbolicznym (asemblerowym). 251. Dokładne listy importu i eksportu pliku źródłowego można otrzymać, badając symbole w odpowiednim pliku obiektowym. 252. Przeglądarki kodu źródłowego umożliwiają przeglądanie dużych kolekcji kodu i klas obiektów. 253. Nie należy ulegać pokusie dostosowywania obcego kodu do własnych standardów kodowania — nieuzasadnione zmiany formatowania tworzą różne bazy kodu i utrudniają zorganizowane zarządzanie kodem. 254. Narzędzia typu pretty-printer oraz mechanizm kolorowania składni mogą uczynić kod źródłowy bardziej czytelnym. 255. Program cdecl' tłumaczy skomplikowane deklaracje typów języka C i C++ do postaci zdań zapisanych w języku angielskim (i odwrotnie). 1 fp://metalab. unc. edu/pub/linux/devel/lang/c/cdecl-2.5. tar.gz.

412

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

256. Wartościowy wgląd w sposób działania programu można często uzyskać poprzez jego uruchomienie. 257. Narzędzia śledzące wywołania systemowe, zdarzenia oraz pakiety mogą zwiększyć zrozumienie działania programu. 258. Programy profilujące pozwalają na dokładne określenie celu wysiłków optymalizacyjnych, zweryfikowanie pokrycia danych wejściowych oraz analizę działania algorytmu. 259. Szukając nigdy niewykonywanych wierszy, można znaleźć słabości w danych testowych i odpowiednio je zmienić. 260. W celu zbadania każdego szczegółu dynamicznego działania programu, który podlega badaniu, należy go uruchomić pod kontrolą debuggera. 261. Kod, który trudno zrozumieć, warto wydrukować na papierze. 262. Warto rysować schematy ilustrujące działanie kodu. 263. Lepsze zrozumienie fragmentu kodu można uzyskać, wyjaśniając go komuś innemu. 264. W celu zrozumienia skomplikowanego algorytmu lub wyrafinowanej struktury danych należy wybrać spokojne i ciche otoczenie i skoncentrować się na nim bez sięgania po pomoc komputera.

Rozdział 11. Pełny przykład 265. Dodawane rozszerzenia programu należy modelować na wzór już istniejących podobnych elementów (klas, funkcji, modułów). Spośród wielu istniejących podobnych elementów należy wybrać posiadający niestandardową nazwę, co uprości przeszukiwanie tekstu w bazie kodu źródłowego. 266. Pliki generowane automatycznie często rozpoczynają się od komentarza określającego ten fakt. 267. Wszelkie próby precyzyjnego przeanalizowania kodu zwykle nakazują zajęcie się wieloma klasami, plikami i modułami, co szybko zaczyna nas przerastać. Dlatego też należy starać się w aktywny sposób ograniczać do absolutnego wymaganego minimum zakres kodu, jaki musimy zrozumieć. 268. Należy stosować ogólną strategię wyszukiwania, starając się podejść do problemu czytania kodu z wielu stron do momentu, aż wreszcie uda się nam znaleźć odpowiednie rozwiązanie.

Bibliografia [AC75]

A. V. Aho i M. J. Corasick. „Efficient string matching: An aid to biblio­ graphic search”. Communications o f the ACM. 18(6): 333 - 340, 1975.

[AGOO]

Ken Arnold i James Gosling. The Java Programming Language, 3rd ed. Boston, MA: Addison-Wesley, 20001.

[AHU74]

Alfred V. Aho, John E. Hopcroft i Jeffrey D. Ullman. The Design and Analysis o f Computer Algorithms. Reading, MA: Addison-Wesley, 19742.

[AIS"77]

Christopher Alexander, Sara Ishikawa, Murray Silverstein, Max Jacob­ son, Ingrid Fiksdahl-King i Shlomo Angel. A Pattern Language. Oxford: Oxford University Press, 1977.

[AKW 88]

Alfred V. Aho, Brian W. Kernighan i Peter J. Weinberger. The AWK Programming Language. Reading, MA: Addison-Wesley, 1988. Christopher Alexander. Notes on the Synthesis o f Form. Cambridge, MA: Harvard University Press, 1964.

[Ale64] [AleOl]

Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied. Boston, MA: Addison-Wesley, 2001.

[AM 86]

Even Adams i Steven S. Muchnick. „Dbxtool: A window-based symbolic debugger for Sun workstations”. Software: Practice & Experience, 16(7): 6 5 3 -6 6 9 , 1986.

[ArmOO]

Phillip G. Armour. „The case for a new business model: Is software a pro­ duct or a medium?”. Communications o f the ACM, 43(8): 1 9 -2 2 , 2000.

[ASU85]

Alfred V. Aho, Ravi Sethi i Jeffrey D. Ullman. Compilers, Principles. Techniques, and Tools. Reading, MA: Addison-Wesley, 1985J.

[Aus98]

Matthew H. Austem. Generic Programming and the STL: Using and Exten­ ding the C++ Standard Template Library. Reading, MA: Addison-Wesley, 1998.

1 Polskie wydanie: JavaTM. WNT, 1999 (tłumaczenie wydania pierwszego). 2 Polskie wydanie: Projektowanie i analiza algorytmów. Helion, 2003. 3 Polskie wydanie: Kompilatory. Reguły, metody i narzędzia. WNT. 2002.

414

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

[Bac86]

Maurice J. Bach. The Design o f the UNIX Operating System. Englewood Cliffs, NJ: Prentice-Hall, 19864.

[BBL95]

Don Bolinger, Tan Bronson i Mike Loukides. Applying RCS and SCCS: From Source Control to Project Control. Sebastopol, CA: O ’Reilly and Associates, 1995. Barry W. Boehm. Bradford Clark, Ellis Horowitz. Ray Madachy. Richard Shelby i Chris Westland. „Cost models for future life cycle processes: COCOMO 2”. Annals o f Software Engineering, 1 :5 7 - 94, 1995. L. Bass, P. Clements i R. Kazman. Software Architecture in Practice. Reading, MA: Addison-Wesley, 1998.

[BCFT95]

[BCK98] [BE93]

Jon Beck i David Eichmann. „Program and interface slicing for reverse engineering”. W: 15th International Conference on Software Engineering, ICSE'93, s. 5 0 9 -5 1 8 . New York: ACM Press, 1993. Kent Beck. Extreme Programming Explained: Embrace Change. Boston, MA: Addison-Wesley, 2000. Jon Louis Bentley. Programming Pearls. Reading, MA: Addison-Wesley. 1986s.

[BecOO] [Ben86] [BF01]

Moshe Bar i Karl Franz Fogel. Open Source Development with CVS. Scottsdale, AZ: The Coriolis Group, 2001.

[BG98]

Kent Beck i Erich Gamma. „Test infected: Programmers love writing tests”. Java Report, 3(7): 37 - 50, 1998.

[BHH99]

L. Barroca, J. Hall i P. Hall (red.). Software Architectures: Advances and Applications. Berlin: Springer Verlag, 1999.

[BK 86]

Jon Louis Bentley i Donald E. Knuth. „Programming pearls: A WEB pro­ gram for sampling”. Communications o f the ACM, 29;(5): 364 - 369, 1986. Jon Louis Bentley, Donald E. Knuth i Douglas Mcllroy. „A literate pro­ gram”. Communications o f the ACM, 19(6): 471 -4 8 3 , 1986. Bruce Blinn. Portable Shell Programming: An Extensive Collection oj Bourne Shell Examples. Englewood Cliffs, NJ: Prentice Hall, 1995.

[BKM 86]

[Bli95] [BMR’96]

Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad i Michael Stal. Pattern-Oriented Software Architecture, Tom 1.: A System o f Patterns. New York: John Wiley and Sons, 1996.

[Boe81]

Barry W. Boehm. Software Engineering Economics. Englewood Cliffs, NJ: Prentice-Hall, 1981. Barry W. Boehm. „The economics of software maintenance”. W Software Maintenance Workshop, s. 9 - 37, Washington, DC, 1983.

[Boe83] [Boe88]

Hans-Juergen Boehm. „Garbage collection in an uncooperative environ­ ment”. Software: Practice & Experience, 18(9): 807 - 820, 1988.

[Bou79]

S. R. Bourne. „An introduction to the UNIX shell”. W: Unix Programmer's Manual [Uni79], Pozycja dostępna również pod adresem http://plan9.belllabs. com/7thEdMan/.

4 Polskie wydanie:

Budowa systemu operacyjnego UNIX. WNT. 1995.

5 Polskie wydanie:

Perełki oprogramowania, WNT. 2001.

Bibliografia

415

[Bra86]

J. Brady. „A theory of productivity in the creative process”. IEEE Computer Graphics and Applications, 6(5): 25 - 34, 1986.

[BRJ99]

Grady Booch, James Rumbaugh i Ivar Jacobson. The Unified Modeling Language User Guide. Reading, MA: Addison-Wesley, 19996.

[Bru82]

Maurice Bruynooghe. „The memory management of Prolog implementa­ tions”. W: Keith L. Clark i Sten-Ake Tamlund (red.), Logic Programming, s. 83 - 9 8 . London: Academic Press. 1982.

[BST89]

H. E. Bal, J. G. Steiner i A. S. Tanenbaum. „Programming languages for distributed computing systems”. ACM Computing Surveys, 21(3): 261 - 322, 1989.

[BTSOO]

Arash Baratloo, Timothy Tsai i Navjot Singh. „Transparent run-time defen­ se against stack smashing attacks”. W: Christopher Small (red.), USENLX 2000 Technical Conference Proceedings, San Diego, CA, June 2000. Ber­ keley, CA: Usenix Association.

[BW98]

Alan W. Brown i Kurt C. Wallnau. „The current state of CBSE”. IEEE Software, 15(5): 3 7 -4 6 , 1998.

[ro i]

Per Cederqvist i in. Version Management with CVS, 2001. Pozycja dostępna również pod adresem http://www.cvshome.org/docs/manuaP(styczeń 2002 ).

[CEK"]

L. W. Cannon, R. A. Elliott, L. W. Kirchhoff i in. „Recommended C style and coding standards”. Artykuł dostępny również pod adresem http://www. dmsl.aueb.gr/dds/res/cstyle/indexw.hlm (grudzień 2001). Zaktualizowana wersja artykułu „Indian Hill C Style and Coding Standards”.

[Chr84]

Thomas W. Christopher. „Reference count garbage collection”. Software: Practice & Experience, 14(6): 503 - 507, 1984. J. O. Coplien, N. Kerth i J. Vlissides. Pattern Languages o f Program Design 2. Reading, MA: Addison-Wesley, 1996.

[CKV96] [ComOO]

Douglas E. Comer. Internetworking with TCP/IP, Tom I: Principles, Proto­ cols and Architecture, 4th ed. Englewood Cliffs, NJ: Prentice-Hall, 2000.

[ConOO]

Damian Conway. Object Oriented Perl. Greenwich, CT: Manning Publi­ cations, 2000 .

[Cre97]

Roger F. Crew. „ASTLOG: A language for examining abstract syntax trees”. W: Ramming [Ram97], s. 229 - 242.

[CS95]

James O. Coplien i Douglas C. Schmidt. Pattern Languages o f Program Design. Reading, MA: Addison-Wesley, 1995. Douglas E. Comer i David L. Stevens. Internetworking with TCP/IP. Tom III: Client-Server Programming and Applications (BSD Socket Version), 2nd ed. Englewood Cliffs, NJ: Prentice-Hall, 1996.

[CS96]

[CS98]

Douglas E. Comer i David L. Stevens. Internetworking with TCP/IP, Tom 11: ANSI C Version: Design, Implementation, and Internals, 3rd ed. Englewood Cliffs, NJ: Prentice-Hall, 1998.

6 Polskie wydanie: UML. Przewodnik użytkownika, WNT 2002.

416

Czytanie kodu. Punkt widzenia twórców oprogramowania open-source

[CT90]

D. D. Clark i D. L. Tennenhouse. „Architectural considerations fora new generation of protocols”. W: Proceedings o f the ACM Symposium on Com­ munications Architectures and Protocols, s. 200 - 208, Philadelphia. PA, 1990. New York: ACM Press.

[CT98]

Tom Christiansen i Nathan Torkington. The Perl Cookbook. Sebastopol. CA: O ’Reilly and Associates, 1998. Ward Cunningham. „Signature survey: A method for browsing unfamiliar code”, 2001. Artykuł dostępny również pod adresem http://c2.com/doc/ SignatureSurvey/ (lipiec 2002). Sprawozdanie z warsztatów OOPSLA 2001 Software Archeology Workshop.

[CunOl]

[CWZ90]

D. R. Chase, W. Wegman i F. K. Zadeck. „Analysis of pointers and struc­ tures”. ACMSIGPLAN Notices, 25(6): 296 - 319, 1990.

[CY79]

Larry L. Constantine i Edward Yourdon. Structured Design. Englewood Cliffs. NJ: Prentice Hall. 1979.

[Dan02]

John Daniels. „Modeling with a sense of purpose”. IEEE Software, 19( 1): 8 - 10, 2002.

[DCS98]

Peter Duchessi i InduShobha Chengalur-Smith. „Client/server benefits, problems, best practices”. Communications o f the ACM, 41(5): 87 - 94, 1998.

[DDZ94]

David Detlefs, AI Dosser i Benjamin Zorn. „Memory allocation costs in large C and C++ programs”. Software: Practice & Experience, 24(6): 5 2 7 -5 4 2 , 1994.

[DE96]

Paul Dubois i Gigi Estabrook. Software Portability with Imake, 2nd ed. Sebastopol, CA: O ’Reilly and Associates, 1996.

[Dij68]

Edsger Wybe Dijkstra. „Go to statement considered harmful”. Commu­ nications o f the ACM, 11(3): 147-148, 1968.

[DijOl]

Edsger W. Dijkstra. My Recollections o f Operating System Design. Wyklad wygłoszony z okazji odebrania tytułu doktora honoris causa na Athens University of Economics and Business, Department of Informatics, 2001.

Bibliografia

417

[El-01]

Khaled El-Emam. „Ethics and open source”. Empirical Software Engineer­ ing, 6(4): 291 -292 ,2 0 0 1 .

[ER89]

Mark W. Eichlin i Jon A. Rochlis. „With microscope and tweezers: An analysis of the internet virus of November 1988”. W: IEEE Symposium on Research in Security and Privacy, s. 326 - 345, Oakland, CA, May 1989.

[ES90]

Margaret A. Ellis i Bjarne Stroustrup. The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley, 1990.

[Fel79]

Stuart I. Feldman. „Make — a program for maintaining computer programs”. Software: Practice & Experience, 9(4): 255 -2 6 5 , 1979.

[FH82]

Christopher W. Fraser i David R. Hanson. „Exploiting machine-specific pointer operations in abstract machines”. Software: Practice