Interface per Makro aufrufen
« | 03 Apr 2019 | »Ich weiß ja:
Makros sind böse!
AAAAAAber maaaaaanchmal gibt es auch ein paar Nischen, wo sie ganz hilfreich sind.
Und um eine nicht-objekt-orientierte Sprache wie C etwas zu “objektivieren”, darf man auch die Regeln ein bisschen beugen.
Objekt-Interfaces sind im GATE Projekt ganz ähnlich aufgebaut wie in
Microsoft’s COM.
Ein Interface ist ein Pointer auf eine Struktur,
von der nur der erste Member bekannt ist, und zwar ein Pointer zur
virtuellen Methodentabelle
(VTBL) und dort befinden sich die
Interfacemethoden.
Und jede Methode führt als ersten Parameter den Pointer zur Struktur,
also den THIS-Pointer.
So weit, so gut.
Aber diese Aufrufe sind in C gelegentlich ziemlich mühsam. Folgender einfacher C++ Code
1object->method(param1, param2);
sieht in C so aus:
1object->vtbl->method(object, param1, param2);
OK, soooo viel mehr an Schreibarbeit ist das hier nicht. Doch das ändert sich, wenn der Objekt-Pointer von wo anders herkommt, z.B.: wenn er ein Member einer anderen Struktur ist.
Das würde dann nämlich so aussehen:
Der eigentliche Objekt-Pointer Teil wird immer länger und muss zusätzlich im ersten Parameter wiederholt werden. Und diese Wiederholung ist nebenbei eine beliebte Fehlerquelle beim Kopieren von Code.
Die Lösung: Ein Makro
Wie schön wäre die Welt doch, wenn so etwas möglich wäre:
1#define INVOKE(obj, method, params) (obj)->vtbl->method((obj), params)
Damit müssen wir den obj
Token nur einmal schreiben.
Doch das Problem sind die params
Hier gibt es zwei Möglichkeiten:
(1) Variable Argumentenliste
Blöd nur, dass Microsoft und der GCC die Syntax anders “sehen” und deshalb besteht diese Lösung aus zwei Implementierungen:
Denn hinter einem Beistrich sind variable Argumente offenbar für den GCC etwas Besonderes, daher der doppelte Hashtoken.
Die Syntax sieht dann so aus:
1INVOKE(object, foobar, param1, param2, param3);
(2) Umklammerung mit Ohne-Umklammerung
Wenn wir nämlich die Syntax in folgender Form wollen,
1INVOKE(object, foobar, (param1, param2, param3));
so sieht das für mich etwas “natürlicher” aus, weil die Parameter
durch die Klammern quasi von object->method
optisch getrennt sind.
Das INVOKE
Makro hat dann genau 3 Parameter, wobei der dritte Parameter
den gesamten Klammerninhalt mit sich führt.
Wäre das Makro so definiert:
1#define INVOKE(obj, method, params) (obj)->vtbl->method((obj), params)
käme folgender Blödsinn heraus:`
1object->vtbl->foobar(object, (param1, param2, param3));
Also müssen die Klammern weg. Und wie macht man das?
Mit einem Trick!
Nachdem params
eine durch Klammern begrenzte Liste anderer Parameter
beinhaltet, wird beim hintereinanderstellen von STRIP_PARENTHESES
und params
durch die Klammern ein “Aufruf” von STRIP_PARENTHESES generiert und dieser
nutzt “normale” __VA_ARGS__
um die Parameterliste einfach einzufügen,
und zwar ohne die ursprünglichen Klammern.
Damit wird das Makro so aufgelöst:
1object->vtbl->foobar(object, param1, param2, param3);
Und wir haben einen ganz validen C - Objektaufruf.
Tja, auch wenn es nicht besonders hilfreich im Alltag ist, so gefallen mir
die beiden Lösungen, und die zweite ist als GOCALL
(für Gate-Object-CALL)
im GATE Projekt implementiert ;)
Microsoft generiert für die C Header aus seinen COM-Klassen pro Methode
ein eigenes Makro, das in etwas so aufgebaut ist:
[RETTYPE] + " " [INTERFACENAME] + "_" + [METHODNAME] + "(" + [INTERFACENAME] + "* thisptr, " + ... + ")"
Das ist auch nicht unschön, aber ohne Codegenerator etwas mühsam. Dennoch verfolge ich diesen Weg bei einigen Klassen (z.B. Streams) ebenso und erzeuge entsprechende Makros manuell.
Für den allgemeinen Fall ist aber der GOCALL
stets einsatzbereit.