Swig
Aus PantheonWiki
Einleitung
Swig ermöglicht es über Interface Dateien C/C++-Quellcode für eine Skriptsprache verfügbar zu machen.
Sämtliche Dateien die das Ruby Interface für Rastullahs Lockenpracht beschreiben, befinden sich in/engine/script/swig.i Dateien werden mit folgender Anweisung
swig.exe -c++ -ruby -minherit -v -Wall (InputPath)zu einer .cxx/.h Datei konvertiert. Dabei benötigen wir eine spezielle Version von SWIG, da ansonsten Fehler in der Skriptsprache den Interpreter zerstören, und damit ein mehrmaliges Testen in RL unmöglich machen.
Die einzelnen Unterprojekte von Rastullahs Lockenpracht besitzen jeweils eine .i und eine .inc Datei in RlScript, in der .i sind die benötigten Header enthalten, in der .inc dagegen die Beschreibung der zu exportierenden Methoden.
Im großen und ganzen lässt sich sagen, dass SWIG ein großes Makropaket ist, das einzelne Textbausteine nimmt und durch Ersetzungen daraus den Quellcode für den Export in die angegebene Skriptsprache generiert.
Export einfacher Klassen
Der Export einer eigenen Klasse mit deren Methoden ist denkbar einfach. Solange keine besonderen Features wie Vererbung über Sprachgrenzen oder automatisches Casten in Ruby benötigt werden, genügt es die passende .i und .inc zu öffnen.
In der .i Datei wird nun der C++ Header inkludiert. In der .inc fügt man an passender Stelle eine Klassenbeschreibung ein, die meist dem Originalheader ähnlich sieht. Will man einzelne Methoden nicht exportieren, so lässt man diese einfach weg, genau wie private Methoden oder Membervariablen. Genug Beispiele findet man in RlCore.inc.
Ein paar kleine Sachen gilt es aber dennoch zu beachten:
- Namespaces immer ganz ausschreiben, auch den rl-Namespace. Aus
Actor* createActor( Real number )
muss alsorl::Actor* createActor( Ogre::Real number )
werden. - Will man das Instanzen dieser Klasse nicht in Ruby erzeugt werden können, genügt es keinen Konstruktor zu exportieren.
- Sollen Instanzen in Ruby erzeugt werden können, muss unbedingt auf die Speicherverwaltung Rücksicht genommen werden.
Export von Klassen mit Vererbungshierarchie
Bisweilen kommt es vor, das man eine komplexe Hierarchie in c++ aufbaut, und diese exportieren möchte. Dabei soll jedesmal bei der Rückgabe der Basisklasse nach Ruby natürlich die jeweils passende höchste Klasse benutzt werden. Dies lässt sich am RL Quellcode besonders gut an RlRules erläutern. Dort gibt es die Klasse GameObject, von der wiederrum Item, Creature als auch Weapon und Person erben. Jedesmal wenn ein GameObject zurückgegeben wird, kann man in Ruby jedoch die wirkliche Klasse benutzen.
Das zugehörige Feature in SWIG nennt sich DYNAMIC. Wenn wir also eine BasisKlasse BaseClass und eine davon abgeleitete Klasse DerivedClass haben, muss folgender Code in die .inc eingefügt werden.
%{
static swig_type_info *BaseClass_dynamic_cast(void **ptr)
{
rl::BaseClass **ppBase = reinterpret_cast<rl::BaseClass**>(ptr);
rl::DerivedClass *pDerived=dynamic_cast<rl::DerivedClass*>(*ppBase);
if( pDerived )
{
*ptr=reinterpret_cast<void*>(pDerived);
return SWIGTYPE_p_rl__DerivedClass;
}
return 0;
}
%}
DYNAMIC_CAST(SWIGTYPE_p_rl__BaseClass, BaseClass_dynamic_cast);
Dieser Block erstellt eine Cast Funktion, die für ein Object von BaseClass versucht, dieses in DerivedClass zu casten. Wenn dies misslingt, ist pDerived NULL. Falls es gelingt gibt man das gecastete Object zurück. Um weitere abgeleitete Klassen hinzuzufügen genügt es den mittleren Block zu duplizieren und anzupassen.
%apply SWIGTYPE *DYNAMIC { BaseClass * };
Dies aktiviert die oben erstellte Castfunktion.
Diesen beiden Blöcke müssen _VOR_ dem Auftreten der Klasse eingefügt werden. Den Rest übernimmt dann SWIG für uns.
Beim Export von abstrakten Klassen müssen die abstrakten Funktionen sowohl in der Basis Klasse als auch in den abgeleiteten Klassen exportiert werden. Die exportierten Klassen nicht funktionieren sonst nicht, weil swig diese Informationen benötigt.
Vererbung von C++ nach Ruby ermöglichen
Eines der tollsten Features unserer Skriptanbindung ist der sogenannte CrosslanguagePolymorphism, oder auf deutsch, die Ableitung der Klassen in Ruby von Klassen in c++. Dies ermöglicht Listener die in Ruby definiert werden können, komplett geskriptete Verhaltenssteuerung und ähnliches, und das 100% Objektorientiert.
Diese Feature nennt sich in SWIG Director. Es lässt sich durch folgende Zeile vor der Klassendefinition der DirectorKlasse in der .inc aktivieren.
%feature("director") DirectorKlasse;
Dies genügt schon, um von dieser Klasse in Ruby abzuleiten, wenn man folgende Dinge zusätzlich beachtet:
- Es muss ein virtueller Destruktor vorhanden sein, und nach Ruby exportiert werden
- Im Ruby Konstruktor muss immer super aufgerufen werden.
- Basisklassen, die einen Nichtstandardkonstruktor definieren dürfen keine abstrakten Methoden enthalten, sonst kann man sie in Ruby nicht ableiten.
- Es müssen alle Ableitungen einer mit %feature("director") ausgezeichneten Klasse entsprechend auszeichnen, wenn man aus Ruby heraus virtuelle Methoden aufrufen will, die nicht in der Basisklasse, sondern in einer davon abgeleiteten Klasse definiert sind.
- Wenn in der in Ruby definierten Klasse ein spezieller Konstruktor definiert sein soll, muss auch immer ein StandardKonstruktor definiert sein!
- Wenn man in Ruby Klassen deklariert, deren Basisklasse abstrakt sind, müssen die pure-virtual-Methoden auch alle definiert werden, sonst werden Laufzeitfehler auftreten. Durch die Swig-Wrapper gibt es keine Compiler-Fehler.
- Alle Aufrufe von potenziell durch Ruby erweiterte Methoden aus C++ müssen mit einem Ausnahmehandler umgeben werden, der gegenfalls auftretende ScriptInvocationFailedException abfangen kann.
Typ Konvertierung von C++ nach Ruby und umgekehrt
Dies passiert mit sogenannten typemaps, die mit der Direktive %typemap definiert werden. Diese erklären swig wie es mit bestimmten Datentypen verfahren soll die von C++ nach Ruby durchgereicht werden und umgekehrt. Dies ist notwendig, weil es ja keine Repräsentation von selbstdefinierten Datenstrukturen von C++ in Ruby von haus aus geben kann.
Die Syntax einer typemap ist wie folgt:
%typemap(typemaptype) c++type, c++type, ...
Für uns sind im wesentlichen die typemaptypes in, out und freearg interessant. in convertiert von ruby nach c++. out konvertiert von c++ nach ruby. freearg ist für den garbage collector von ruby von interesse.
STL Container Typ Konvertierungen
Typemaps kommen immer dann zur Anwendung, wenn Variablen zwischen C++ und Ruby konvertiert werden müssen (die Gründe dafür können verschieden Ursachen haben - für nähere Erläuterungen sei auf die Swig doku verwiesen). Nun STL container stellen diesbezüglich eine Sonderstellung dar, weil sie in C++ häufig verwendet werden und es nicht trivial ist, diese in eine typemap zu definieren. Die Swig Entwickler waren aber so freundlich und haben in diese Richtung eine Menge Vorarbeit geleistet in Form von sogenannten templates. Diese sind eine relativ neue Entwicklung, deshalb findet man auch recht spärlich nur Informationen in diese Richtung und daher fällt die Anwenung nicht gerade leicht. In Folge wird nun erklärt wie diese Erweiterung zu nutzen ist. (Im zweifelsfalle gilt immer das was in swig/Examples/ruby/std_vector zu finden ist)
z.b. man hat eine funktion mit folgendem kopf:void test(std::vector< EineKlasse >& KlassenListe)und exportiert diese Funktion nach Ruby. Damit swig nun einen Konvertor für std::vector< EineKlasse >& besitzt muss man _VOR_ der definition des Exports
%template(EineKlasseVector) std::vector< EineKlasse >;definieren. In Ruby muss man dann den parameter, den man der Funktion übergibt vorinitialisieren mit
var = EineKlasseVector.new() test(var).
Hinweis: Die C++ Klasse EineKlasse muss _VOR_ der definition des %template(EineKlasseVector) std::vector< EineKlasse >; definiert werden.
Ein Beispiel in RL, wie man das in der Praxis macht findet sich in RlAi.swig mit AStar::search(std::vector<Ogre::Vector3> resultPath); .
Exceptionhandling zwischen Ruby und C++
C++ Exceptions in Ruby
Alle Exceptions die bei Aufruf einer c++ Methode auftreten, werden aufgefangen und in Ruby-Exceptions umgewandelt, und ermöglichen konsistentes Verhalten.
Alle Aufrufe die aus Ruby möglich sind, dürfen maximal eine Exception auslösen, alle dummen, ja noch so idiotischen Eingaben müssen gefiltert werden, also also Werte auf NULL überprüfen und Division durch 0 verhindern.
Ruby Exceptions in C++
Die einzigen Stellen an denen Ruby Code aus C++ Code direkt aufgerufen wird, ist beim Aufruf von DirectorMethoden, also überschreibbaren Methoden in Directorklassen. Wenn diese nicht aufgefangen werden, kann ein einfacher Fehler in Ruby eine Exception und somit einen Absturz verursachen.
Alle Ruby Exceptions werden zu einer ScriptInvocationFailed Exception umgewandelt, die unbedingt abgefangen werden muss. Dabei ist ein ExceptionHandler an jeder Aufrufstelle einer möglicherweise in Ruby implementierten Funktion nötig.
try
{
directorMethod( );
}
catch( ScriptInvocationFailedException& sife )
{
Logger::getSingleton().log(Logger::CORE, Logger::LL_ERROR, sife.toString() );
}
Speicherverwaltung zwischen Ruby und C++
Die Speicherverwaltung zwischen Ruby und c++ hat wie das ExceptionHandling zwei Seiten. Jedoch wird dies an einer Stelle verwaltet, vom ScriptWrapper in common.
Um den ScriptWrapper zu benutzen genügt es den Header zu inkludieren und sich per Singleton die Instanz zu holen.
#include "ScriptWrapper.h" ScriptWrapper::getSingleton().owned( obj ); ScriptWrapper::getSingleton().disowned( obj ); ScriptWrapper::getSingleton().deleted( obj );
RubyObject in C++ Besitz
Die einfachste Situation. Ein Objekt wurde per new in c++ erzeugt und ist irgendwie nach Ruby rausgegeben worden. Dies ist z.B. bei Actor der Fall, dieser wird über den ActorManager erzeugt, und auch vom ActorManager gelöscht. Jederzeit kann sich Ruby einen Zeiger auf einen Actor geben lassen. Wird dieser Actor jedoch in c++ gelöscht, bekommt Ruby dies nicht mit, der nächste Zugriff auf unseren Zeiger innerhalb Rubys zeigt auf ungültigen Speicher und führt zu einem Absturz.
Um dies zu verhindern müssen wir Ruby mitteilen, dass das Object gelöscht worden ist. Dies geschieht mit dem Aufruf
ScriptWrapper::getSingleton().deleted( obj );
Dabei wird die Ruby Instanz mit nil überschrieben, und es werden sämtliche Verknüpfungen zwischen C++ und Ruby gelöst. Dies muss für _JEDE_ nach Ruby exportierte Klasse gemacht werden, die in c++ gelöscht wird!
C++ Object in Ruby Besitz
Das Ganze in kompliziert. Alle Objekte die in Ruby per .new erzeugt worden sind, befinden sich unter Kontrolle des Ruby Garbage Collectors. Stellt Ruby fest, das kein weiteres Ruby Object auf dieses Object zeigt, wird es gelöscht. Jedoch weiß Ruby nicht, ob oder wie oft die Instanz in C++ genutzt wird. Beim nächsten Zugriff der in c++ erfolgt haben wir wieder ungültigen Speicher und einen Absturz.
Um den übereifrigen GarbageCollector einzuschränken gibt es nur eine Möglichkeit, wir müssen uns merken wo wir in c++ Verweise auf dieses Object speichern. Jedesmal wenn wir ein weiteres Mal das Object in einer Instanzvariablen speichern oder in einen Container einfügen, den wir später noch benutzen wollen, müssen wir dies melden, jedesmal wenn wir es aus einem Container löschen oder gegen ein anderes Objekt tauschen, müssen wir auch das melden. Dies geschieht mit den Aufrufen:
ScriptWrapper::getSingleton().owned( obj ); ScriptWrapper::getSingleton().disowned( obj );
Dabei wird im ScriptWrapper ein ReferenceCount aufrechtgehalten, anhand dessen festgestellt ob weiterhin Ruby mitgeteilt werden soll, das das Object noch nicht freigegeben ist. Dies muss für _JEDE_ Klasse gemacht werden, deren Konstruktor nach Ruby exportiert wird.
Dokumentation der Skripte
Hier suche ich immer noch.
Typemaps für einfache Datentypen
Es existieren jede Menge typemaps für einfache Datentypen, die keinen weiteren Aufwands bedürfen um sie in Ruby zu benutzen. Dazu gehören:
- Real
- String
- StringVector
- CeguiString
- CeGuiStringVector
- pair
- Tripel
- Vector3
- Quaternion
- Radian
- ColourValue
FAQ
Diese FAQ ist nicht unbedingt aktuell, bitte nicht unbedingt wörtlich nehmen.
- Generell gilt: SWIG ist ein großes Makropaket, dass im wesentlichen mittels Suchen/Ersetzen die .i-Datei in eine .cxx-Datei umwandelt. Wenn SWIG deinen Quelltext akzeptiert bedeutet das gar nichts. Es werden nur rudimentäre Fehlerüberprüfungen gemacht und praktisch keine Plausibilitätstest.
- Problem: Was muss ich machen, damit Swig mit abstrakten Klassen umgehen kann?
- Lösung: Es gibt mehrere Lösungen:
- SWIG versteht virtual T doSomething() = 0; und erzeugt keine Konstruktoren oder Destruktoren für die entsprechende Klasse.
- Durch %nodefault; wird die Erzeugung von Konstruktoren und Destruktoren für die folgenden Klassen abgeschaltet, mit %makedefault; wieder angeschaltet.
- nodefault akzeptiert auch Klassennamen, %nodefault Klasse; schaltet das Erzeugen von Konstruktoren und Destruktoren für die Klasse #Klasse# ab.
- Problem new: allocator undefined for classname
- Lösung: Dieses Problem tritt bei der Erzeugung einer abstrakten Klasse in ruby auf. Meist wurde von einer abstrakten Basisklasse geerbt und in einer nicht-abstrakten Unterklasse eine virtuelle Methode nicht explizit in der .swig-Datei angegeben wurde. Einfach alle vormals virtuellen Methoden in nicht abstrakten Klassen angeben.
- Problem: Klasse mit director-Auszeichnung kann aus Ruby nicht erzeugt werden. Fehlermeldung ist "No matching function to overload new_Classname."
- Lösung: Die Klasse hat mehrere überschriebene Konstruktoren mit unterschiedlicher Argumentzahl. Das führt zu dem Problem. Workaround ist erstmal nur einen Konstruktor zu exportieren. Der Quelltext weist aus, dass SWIG jeweil von einem Parameter mehr ausgeht, vielleicht ein Fehler, vielleicht aber auch ein Workaround ihrerseits.
Wichtige Links
- Swig
- Ruby in Swig - Dokumentation zur Umsetzung von C++ nach Ruby.
