Interface per Makro aufrufen

Ich weiß ja:

Makros sind böse!

AAAAAAber maaaaaanchmal gibt es auch ein paar Nieschen, 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

object->method(param1, param2);

sieht in C so aus:

object->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:

parent->child->method(param1, param2);
parent->child->vtbl->method(parent->child, param1, param2);

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:

#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:

#if defined(GATE_COMPILER_MSVC)
#define INVOKE(obj, method, ... ) \  
  (obj)->vtbl->method((obj), __VA_ARGS__ )
#else
#define INVOKE(obj, method, ... ) \  
  (obj)->vtbl->method((obj), ##__VA_ARGS__ )
#endif

Denn hinter einem Beistrich sind variable Argumente offenbar für den GCC etwas Besonderes, daher der doppelte Hashtoken.

Die Syntax sieht dann so aus:

INVOKE(object, foobar, param1, param2, param3);

(2) Umklammerung mit Ohne-Umklammerung

Wenn wir nämlich die Syntax in folgender Form wollen,

INVOKE(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:

#define INVOKE(obj, method, params) (obj)->vtbl->method((obj), params)

käme folgender Blödsinn heraus:`

object->vtbl->foobar(object, (param1, param2, param3));

Also müssen die Klammern weg. Und wie macht man das?
Mit einem Trick!

#define STRIP_PARENTHESES(...) __VA_ARGS__
#define INVOKE(obj, method, params) \  
  (obj)->vtbl->method((obj), STRIP_PARENTHESES params)

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:

object->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.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!