tag, userdata, userparam und LPARAM

Wenn man eigene Konstrukte mit jenen einer anderen C Bibliothek verknüpfen muss, braucht man mindestens an einer Stelle ein Verbindungsglied oder einen Ankerhaken, mit dem man Datenstrukturen, die sich gegenseitig nicht kennen, verknüpfen kann.

Und genau deshalb hat jede “gute” C-Bibliothek in seinen Objekten (bzw. structs) ein Feld, wo man einen Fremdwert eintragen kann, der überall hin mitgenommen werden kann …
… also ein freien Parameter für den Anwender: ein userparam


Im GATE Projekt sind viele Objekte Wrapper um native OS-spezifische Objekte. Das gilt vor allem bei GUI Operationen, woran ich zur Zeit wieder etwas weiterarbeite.

Nun möchte ich, dass eine generische C-Struktur im GATE-Framework unter allen Systemen ohne Anpassung nutzbar wird. Das lässt sich am einfachsten mit einem Pointer und dem PIMPL Idiom (Pointer to IMPLementation) realisieren:
Jedes “äußere” Objekt hält einen Pointer zur nativen Implementierung. Und je nach Plattform sieht diese dann anders aus, während sich das Objekt selbst immer gleich nach außen präsentiert.

Das funktioniert aber nur in eine Richtung, nämlich vom GATE Framework zur nativen Komponente. Für den anderen Weg, z.B. Callbacks für Events, braucht die Komponente irgend einen “Marker” um das sie kapselnde Objekt zu finden.

Aus der Windows UI Programmierung hat die WinAPI auch für viele Nicht-UI-Bereiche den LPARAM etabliert. Dieser Integer hat stets eine Pointer-Größe (void*), weshalb man auch einen auf LPARAM gecasteten Pointer auf diese Weise übergeben kann.

Man hat also sein eigenes Objekt (oder die eigene Datenstruktur), ruft die API mit der Adresse dazu auf und kann im Callback über den zuvor gesicherten Parameter wieder zu seinem eigenen Objekt zurückkommen.

Doch was macht man, wenn dieser “Parameter” nicht ausreicht, um das abzubilden, was man braucht?

Fehlerfall: int oder gar nichts

Ein userparam ist nur dann hilfreich, wenn er vom Typ void*, intptr_t oder uintptr_t ist. All jene, die nur einen int oder unsigned int dafür gebrauchen, haben meist schon verloren.
Denn int ist auf einigen Plattformen kleiner als intptr_t (z.B. Win64) und braucht laut C-Standard überhaupt nur 16-bit groß sein.

Das teuflische dabei: Alter Code (16/32 bit) scheint portabel zu sein, doch beim Einsatz auf z.B. einer 64-bit Architektur hängt es vom OS ab, ob sich ein Objektzeiger ohne Bitverlust in 32 Bits darstellen lässt.

Genau so schlimm ist es, wenn Objekte gar keine Möglichkeit einbringen, mit eigenen Strukturen verknüpft zu werden.

Was soll man dann tun?

Lösung: Verknüpfungs-Registrierung implementieren

Ich hasse mich dafür es laut zu sagen, aber die Lösung heißt:
Globale Variablen und zwar globale Mapping-Tabellen, die eine fremde Ressource mit der eigenen verknüpfen.

Dafür brauchen wir mindestens:

  • Eine Funktion zur Verknüpfung
    e.g. void register_object_link(void* foreign_object, void* my_object)
  • Eine Funktion zur Lösung dieser Verknüpfung
    e.g. void unregister_object_link(void* foreign_object)
  • Eine Funktion zur Verfolgung der Verknüpfung
    e.g. void* resolve_my_object(void* foreign_object)
  • Optional auch noch eine Funktion zur Rückverfolgung einer Verknüpfung
    e.g. void* resolve_foreign_object(void* my_object)

Mit C++ wäre das mit einer einfachen std::map leicht realisierbar. Allerdings sollte man auch stets noch einen globalen Mutex beimengen, denn wenn Registrierungen und Auflösungen aus unterschiedlichen Threads eintreffen schafft eine solche globale Registrierungsstelle mehr Probleme als Lösungen.

Diese Notfall-Strategie wird im GATE Projekt an einigen Stellen genutzt um GATE-Objekte an ihr natives Gegenstück zu koppeln, damit man aus Callbacks der nativen Welt in die GATE-Welt und umgekehrt wechseln kann.


Ein ganz besonders übler Fall von Callback befindet sich in Bibliothek libssh2. Siehe libssh2_userauth_keyboard_interactive_ex
Wer einen interaktiven Login durchführen will (was ja recht häufig bei SSH passiert), muss dies über einen Callback realisieren, der nicht einmal mit dem Objekt verknüpft ist, das den Callback überhaupt erst auslöst.
Man dachte wohl, “interactive” bedeutet, man liest von STDIN und gibt die eingetippten Zeichen einfach zurück.
Doch damit kann man die libssh2 nicht mehr als flexible Komponente und schon gar nicht multithreaded ausführen.

Die einzige Lösung, die mir bei so einer Implementierung noch einfällt, ist die gesamte SSH-Login Aufrufprozedur durch einen Mutex zu schützen und die Kommunikation über weitere globale Variablen durchzuführen.
… viel hässlicher geht’s ja wohl nicht mehr … aber was soll man sonst tun?

Daher, liebe Bibliotheken Entwickler, vergesst nicht:

Jedes öffentliche Objekt sollte die Möglichkeit bereitstellen, mit einem Fremdobjekt verknüpft zu werden! JEDES!