Kleiner Einsteigerguide, Teil 1: Der erste Patch

Moderator: Moderatoren2

Antworten
Ed von Schleck
Soldat
Soldat
Beiträge: 15
Registriert: Mi 12. Dez 2012, 13:21

Kleiner Einsteigerguide, Teil 1: Der erste Patch

Beitrag von Ed von Schleck »

Hallo wieder,

In diesem Teil des Einsteigerguides geht es direkt zur Sache: Wir produzieren den ersten Patch. Ihr werdet noch keinen großen Überblick über die Codebasis als Ganzes erhalten, weil ich glaube, dass man sich so etwas am besten erarbeitet, indem man im Code herumwühlt.

Darum geht es heute um zwei Dinge - erstens: Eine kleine (unvollständige) Einführung in Referenzen und const-correctness, und zweitens: Wie benutze ich Mercurial (unser Versionskontrollsystem, Ihr erinnert Euch), um einen Patch zu produzieren.

Wechseln wir zuerst in das Verzeichnis, in das wir den ASC Quellcode geklont haben:

Code: Alles auswählen

cd ~/code/asc/
oder wie auch immer Euer Verzeichnis heißt. Die Tilde meint übrigens Euer home-Verzeichnis. Schauen wir uns mal um: Wir sehen eine Menge Datein und Verzeichnisse. Wir brauchen uns damit erst einmal nicht zu beschäftigen - uns interessiert vor allem ein Verzeichnis namens source. Also

Code: Alles auswählen

cd source
und wieder umschauen: Hier sehen wir eine Menge Dateien, die auf .cpp und .h enden, sowie ein paar Unterverzeichnisse, ebenfalls mit .cpp- und .h-Dateien drin. Wo sollen wir jetzt bloß anfangen? Die Antwort für jetzt sei einfach mal: Irgendwo (später werden wir sinnvollere Antworten finden, versprochen). Um irgendeinen Anhaltspunkt zu haben, nehmen wir die Datei autotraining.cpp, denn diese Datei ist schön klein und übersichtlich :).

der Plan

Unser Plan ist einfach: Wir versuchen, eine Funktion zu finden, in der eine Referenz zu irgendwas übergeben wird, aber das Objekt, auf das verwiesen wird, nicht verändert wird, also konstant bleibt. Wenn es konstant bleibt, können wir den Parameter mit dem Keyword const kennzeichnen - das sagt dem Compiler: Dieser Parameter wird in der Funktion nur lesend verwendet. Wichtiger ist aber, dass es dem Programmierer kennzeichnet, wie der Parameter verwendet wird. Wenn ich also als Entwickler die Funktionssignatur sehe (als Signatur bezeichnet man die Parameter und den Rückgabetyp einer Funktion), dann weiß ich schon: Aha, dieser Parameter wird nicht verändert.

Das ist wichtig, denn wenn ich eine Funktion aufrufe, die eine nicht-const Referenz als Parameter nimmt, dann kann sie mit der Variable, die ich Ihr übergebe, irgendwas anstellen, und hinterher ist die nicht mehr die gleiche. Um sicherzugehen, muss ich mir die Implementation der Funktion anschauen, und das ist nervig und fehleranfällig. Ist der Parameter aber const, dann kann ich ihr bedenkenlos eine Variable als Parameter übergeben, ohne in die Implementation der Funktion hineinzuschauen, weil ich weiß, dass ich die Variable nach dem Funktionsaufruf genauso aussieht wie davor.

Also: Öffnen wir die Datei autotraining.cpp in unserem Editor (z.B. Geany). Schauen wir uns die Datei an: Es werden zwei Funktionen definiert. In der zweiten gibt es einen Parameter, der als nicht-const-Referenz übergeben wird: Der Paramter player. Wenn wir uns die Funktion so anschauen, dann scheint es nicht so zu sein, als würde auf den Inhalt der Referenz schreibend zugegriffen, daher ändern wir die Signatur von

Code: Alles auswählen

void automaticTrainig( GameMap* gamemap, Player& player )
gemäß unseres Planes in

Code: Alles auswählen

void automaticTrainig( GameMap* gamemap, const Player& player )
Zu der .cpp-Datei gibt es noch eine .h-Datei namens autotraining.h. Diese sogenannte Header-Datei definiert eine Art Schnittstelle, um die in der .cpp-Datei implementierten Funktionen zuzugreifen. Andere .cpp-Dateien können sich diese Header-Datei mit der #include Anweisung einverleiben und damit auf die Funktionen zugreifen, die darin deklariert werden, ohne dass sie die Implementation kennen müssen. Der Linker (ein Programm, dass nach dem Kompiler ausgeführt wird) verknüpft dann die Deklarationen mit den Implementationen.

die Änderung

Logischerweise muss die Deklaration mit der Implementation übereinstimmen. Öffnen wir die Datei autotraining.h und ändern wir die Zeile

Code: Alles auswählen

 extern void automaticTrainig( GameMap* gameMap, Player& player );
in

Code: Alles auswählen

 extern void automaticTrainig( GameMap* gameMap, const Player& player );
Jetzt tippen wir in das Terminal

Code: Alles auswählen

make -j
und schauen, was passiert.

das erste Problem

Da wir zwar eine Funktion geändert haben, aber noch nicht die Stellen im Code, die diese Funktion aufrufen, werden wir auf jeden Fall mindestens eine Fehlermeldung bekommen. Und da ist sie auch schon

Code: Alles auswählen

[...]
make[3]: Entering directory `/home/christian/src/ascdefault/asc/source/unittests'
[...]
main.cpp: In function 'int runTester()':
main.cpp:140:76: error: no matching function for call to 'SigC::Signal2<void, GameMap*, Player&>::connect(SigC::Slot2<void, GameMap*, const Player&>)'
main.cpp:140:76: note: candidate is:
In file included from /usr/include/sigc++-1.2/sigc++/sigc++.h:27:0,
                 from ../../source/ai/../util/messaginghub.h:28,
                 from ../../source/ai/../typen.h:42,
                 from ../../source/ai/ai.h:34,
                 from main.cpp:6:
/usr/include/sigc++-1.2/sigc++/signal.h:723:18: note: SigC::Connection SigC::Signal2<void, P1, P2, Marsh>::connect(const InSlotType&) [with P1 = GameMap*; P2 = Player&; Marsh = SigC::Marshal<void>; SigC::Signal2<void, P1, P2, Marsh>::InSlotType = SigC::Slot2<void, GameMap*, Player&>]
/usr/include/sigc++-1.2/sigc++/signal.h:723:18: note:   no known conversion for argument 1 from 'SigC::Slot2<void, GameMap*, const Player&>' to 'const InSlotType& {aka const SigC::Slot2<void, GameMap*, Player&>&}'
[...]
Whoa. Was heißt das jetzt?

Offenbar geht irgendwas in der Datei unittests/main.cpp in Zeile 140 mächtig schief. Schauen wir uns mal diese Zeile an:

Code: Alles auswählen

   GameMap::sigPlayerTurnEndsStatic.connect( SigC::slot( automaticTrainig ));
Was bedeutet das nun? Da muss ich ein wenig ausholen.

Signale und Slots

ASC verwendet für viele Dinge ein sogenanntes Signal- (oder Signal/Slot-) Framework namens SigC++. Es gibt auch andere solche Frameworks, aber das Prinzip dahinter ist immer gleich:

Ich definiere irgendwo ein Signal. Das Signal ist eine Art Objekt, mit dem ich Funktionen verbinden kann. Das passiert in der Zeile oben mit der Methode connect. Diese Funktionen müssen aber in ein Slot passen. Ein Slot ist nicht viel mehr als eine Funktionssignatur. Alle Funktionen, die eine passende Signatur aufweisen, können also mit diesem Signal verbunden werden. Das Signal kann dann von allen möglichen Stellen im Code getriggered werden. Alle Funktionen, die damit verbunden sind, werden dann ausgeführt.

Warum macht man das? Warum führt man nicht einfach gleich die richtigen Funktionen aus? Die Antwort ist: Der Signal-Mechanismus erlaubt, sozusagen quer zu programmieren. Angenommen, ich möchte eine Funktion immer dann ausführen, wenn eine Runde endet. Dann könnte ich entweder die Stelle im Code suchen, wo das Rundenende definiert wird. Wenn ich Pech habe, gibt es verschiedene Stellen im Code, wo ein Rundenende eingeläutet werden kann.

Praktischer ist es da, wenn ich weiß, dass zum Ereignis "Rundenende" immer das Signal "Rundenende" getriggered wird. Dann muss ich nur meine Funktion mit diesem Signal verbinden und voila - Es funzt, ohne dass ich überall in meiner Codebasis rumwühlen muss.

Zurück zu unserer Codezeile oben: Da ich die Funktionssignatur unserer Funktion geändert habe, passt sie also nicht mehr in das Slot. Ich muss nun die Slot-Definition anpassen. Wie mache ich das? Die Codezeile verrät eigentlich alles: Das Signal ist eine Eigenschaft der Klasse GameMap und heißt sigPlayerTurnEndsStatic. Also öffnen wir die Dateien gamemap.cpp und gamemap.h und verändern in gamemap.h:

Code: Alles auswählen

      static SigC::Signal2<void,GameMap*,Player&> sigPlayerTurnEndsStatic;
in

Code: Alles auswählen

      static SigC::Signal2<void,GameMap*, const Player&> sigPlayerTurnEndsStatic;
und in gamemap.cpp

Code: Alles auswählen

SigC::Signal2<void,GameMap*,Player&> GameMap::sigPlayerTurnEndsStatic;
in

Code: Alles auswählen

SigC::Signal2<void,GameMap*, const Player&> GameMap::sigPlayerTurnEndsStatic;
Jetzt wieder

Code: Alles auswählen

make -j
das zweite Problem

Jetzt kommt bei mir eine neue Fehlermeldung:

Code: Alles auswählen

[...]
./../../autotraining.cpp: In Funktion »void automaticTrainig(GameMap*, const Player&)«:
./../../autotraining.cpp:64:71: Fehler: Umwandlung von »std::list<Building*>::const_iterator {aka std::_List_const_iterator<Building*>}« in nicht-skalaren Typen »std::list<Building*>::iterator {aka std::_List_iterator<Building*>}« angefordert
./../../autotraining.cpp:67:68: Fehler: Umwandlung von »std::list<Vehicle*>::const_iterator {aka std::_List_const_iterator<Vehicle*>}« in nicht-skalaren Typen »std::list<Vehicle*>::iterator {aka std::_List_iterator<Vehicle*>}« angefordert
[...]
Was soll das jetzt schon wieder?

Iteratoren

Schauen wir uns die erste Zeile an, über die der Kompiler meckert (und die nächste gleich mit):

Code: Alles auswählen

   for ( Player::BuildingList::iterator b = player.buildingList.begin(); b != player.buildingList.end(); ++b )
      autoTrainer( *b );
Hier geht es um sogenannte Iteratoren. Ein Iterator ist eine Art Hilfsobjekt. Der Name deutet es schon an: Es hilft dabei, über eine Menge von Daten zu iterieren. Hier zum Beispiel gibt es eine Liste von Gebäuden, die ein Spieler hat, und ich möchte eine Funktion (hier: autoTrainer) für jedes dieser Gebäude ausführen. Dann fordere ich von der Liste einen Iterator an. Er ist wie ein Pointer, der auf ein einzelnes Gebäude zeigt. Wenn ich den Operator ++ darauf ausführe, dann zeigt er auf das nächste Gebäude.

Wie aber sehe ich, ob ich nicht schon durch bin mit den Gebäuden? Dafür gibt es einen speziellen Wert, den die List mit der Methode end() zurückgibt. Er deutet an: Die Liste ist zu Ende. Also überprüfe ich jedes mal, ob mein Iterator diesen Wert hat. Wenn nicht, mache ich weiter. Wenn ja, breche ich ab.

Was ist nun mit der Funtion oben falsch? Das hat offensichtlich mit dem const zu tun, das wir eingefügt haben - etwas anderes haben wir ja nicht verändert. In der Tat gibt es zwei unterschiedliche Typen von Iteratoren: Normale und konstante Iteratoren. Ein konstanter Iterator ist nicht vom Typ iterator, sondern vom Typ const_iterator. Also müssen wir in der Zeile oben den Typ des Iterators ändern:

Code: Alles auswählen

   for ( Player::BuildingList::const_iterator b = player.buildingList.begin(); b != player.buildingList.end(); ++b )
      autoTrainer( *b );
Ebenso für die Schleife untendrunter:

Code: Alles auswählen

   for ( Player::VehicleList::const_iterator v= player.vehicleList.begin(); v != player.vehicleList.end(); ++v )
      autoTrainer( *v );
Und schon wieder

Code: Alles auswählen

make -j
Das dauert ein wenig, denn die Datei gamemap.hwird an vielen Stellen #included. Aber zumindest bei mir klappt alles. Hurra!

Commit

Wir wollen unsere großartige Verbesserung natürlich mit dem Rest der Welt teilen. Dazu committen wir unsere Änderung, das heißt: Wir teilen unserem Versionscontrolsystem mit, dass es Änderungen gegeben hat. Dazu geben wir eine kleine Beschreibung an, was wir geändert haben, so dass nachfolgende Generationen wissen, was in dem Commit passiert ist.

Bevor wir den Commit absetzen, richten wir Mercurial noch ein. Das ist wichtig, damit andere den Commit mit unserer Person in Verbindung bringen können. Legt dazu eine Datei namens .hgrc direkt in Eurem home-Verzeichnis an und schreibt rein:

Code: Alles auswählen

[ui]
username = Ed von Schleck <ed.von.schleck@test.com>
Natürlich mit Eurem eigenen Namen und eMail-Adresse.

Jetzt tippt (wieder im asc-Quellcode-Verzeichnis) Folgendes:

Code: Alles auswählen

hg commit
Es öffnet sich ein Fenster eines Editors. Welcher das ist, wird von der Umgebungsvariable $EDITOR bestimmt (es lohnt sich, diese Variable zu ändern; googelt nach "set editor variable" und Eurer Linux-Distribution). In diesem Editor schreibt nun eine Beschreibung Eures Patches.

Es ist üblich, eine Kurzzusammenfassung zu schreiben, die in eine Zeile passt (eine Zeile ist weniger als 80 Zeichen), und darunter einen oder mehrere Paragraphen mit einer ausführlichen Beschreibung. Dieser Patch fällt eher in die Kategorie "simpel", daher könnte man die ausführliche Beschreibung auch weglassen. Diese Commitmessages sind immer auf Englisch (wie auch Kommentare im Code). Ich schreibe also an den Anfang der Datei

Code: Alles auswählen

Changed 'player' parameter type in 'automaticTraining' to const reference

In the process I had to change the 'sigPlayerTurnEndsStatic' slot
definition as well.
Dann speichere ich und schließe den Editor. Ich will nun sichergehen, dass meine Änderung wirklich übernommen wurde und schaue mir die History an

Code: Alles auswählen

hg history | less
und sehe ganz oben:

Code: Alles auswählen

Änderung:        3339:308acef4383f
Marke:           tip
Vorgänger:       3336:e7a86748078d
Nutzer:          Christian Schramm <christian.h.m.schramm@gmail.com>
Datum:           Tue Apr 02 18:39:44 2013 +0200
Zusammenfassung: Changed 'player' parameter type in 'automaticTraining' to const reference
Super! Nur haben die anderen noch nichts davon. Ich kann jetzt meine Änderung auf zwei Arten verteilen: Die Oldschool-Methode mit einem Patch-File, oder Newschool mit einem Pull-Request. Ich erkläre hier mal beide.

ein Patch-File erstellen

Dafür gibt es eine praktische Funktion von Mercurial: Ich kann ihm sagen, ein diff auszuspucken, dass die Änderungen meines letzten Commits beschreibt. Dazu schreibe ich einfach

Code: Alles auswählen

hg export tip
tip ist ein Kürzel für das letzte Commit. Mercurial gibt nun das diff direkt im Terminal aus. Ich will das diff aber in eine Datei schreiben. Das machen wir ganz UNIXy mit dem >-Operator:

Code: Alles auswählen

hg export tip > patch0.diff
Jetzt haben wir eine Datei patch0.diff rumfliegen. Darin sind die Änderungen enthalten. Wenn wir diese Date an TheCoder oder schicken, dann kann er das direkt einbauen.

Ein Patch-File ist praktisch, um einen Überblick über die Änderungen zu kriegen. Es ist aber manchmal etwas frickelig, diese wieder zu integrieren, insbesondere wenn es viele und komplexe Änderungen sind. Daher gibt es eine zweite Methode.

der Pull-Request

Dazu brauchen wir ein Mirror unseres Codes. Es gibt Services im Internet, die so etwas bereitstellen. Der verbreitetste Service für Mercurial ist bitbucket.org. Da kann man sich anmelden und sein Repository spiegeln. Ich erkläre jetzt nicht, wie das geht; die Seite ist recht selbsterklärend, aber wenn Ihr Fragen habt, postet sie einfach unten.

Wenn Ihr also ein leeres Repository auf bitbucket eingerichtet habt, müsst Ihr es als remote eintragen, indem Ihr in das Unterverzeichnis .hg in die Datei hgrc in die Sektion [path] etwas Equivalentes zu

Code: Alles auswählen

bitbucket = https://edvonschleck@bitbucket.org/edvonschleck/asc
eintragt. Dann macht

Code: Alles auswählen

hg push bitbucket default
Damit ist Euer Mirror bei bitbucket up-to-date.

Wenn Ihr dann TheCoder oder mir Eure Änderungen geben wollt, dann schreibt uns einfach die Adresse zu Eurem Mirror, und wir können die Änderungen selbst ziehen und in unseren Code mergen.

So! Ich hoffe, Ihr habt das alles direkt am lebenden Code nachvollzogen. Wenn nicht: Macht es! Ihr braucht die Übung, denn jetzt kommt (Trommelwirbel)

die Aufgabe

Findet selbst eine solche Stelle im Code, wo ein Parameter nicht const ist, aber sein sollte, und verbessert die Stelle so lange, bis es wieder kompiliert! Wenn Ihr auf Probleme stoßt (und das werdet Ihr wohl :) ), dann postet das Problem, ich werde helfen. Und wenn Ihr es geschafft habt: Postet den Patch und einen Link zu Eurem Repository. Meinen Respekt werdet Ihr Euch verdient haben, denn das ist nicht ohne.

Alles klar? Dann mal los!
TanzAufDemVulkan
Soldat
Soldat
Beiträge: 4
Registriert: Mi 26. Dez 2012, 11:37

Re: Kleiner Einsteigerguide, Teil 1: Der erste Patch

Beitrag von TanzAufDemVulkan »

:D :D :D :D :D
Antworten