tag, userdata, userparam und LPARAM
« | 23 Feb 2020 | »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. struct
s) 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!