ich habe das Problem, daß der Callback auf eine Memberfunktion der Dialogklasse nicht funktioniert, wenn der Callback-Aufruf in der vom Timerhandler aufgerufenen Funktion steht. Der Aufruf an sich funktioniert aber definitiv außerhalb des Timerhandlers und auch der Timerhandler selbst wird nachweislich abgearbeitet !
Zum Code:
Aus der Dialogklasse "CTabDialog" wird mit der Methode "CModulTime::StartModulTime" ein Timer gestartet, der dann zyklisch den Callback "call()" auf die Member-Funktion der Dialogklasse machen soll. fp ist ein Objekt der Klasse FunctionPointer aus der mbed Microcontroller Library, hier schon mal verlinkt in:
Beitrag "Re: C++ member Methode als Callback-Funktion übergeben"
(Mit der Klasse sind auch Callbacks auf nicht-statische Memberfunktionen möglich).
Ich nehme an, daß es irgendwie am Callback-Aufruf der statischen Timerfunktion liegt, habe aber keinen Schimmer, was genau das Problem ist. Das ganze kostet mich jetzt schon paar Stunden, ohne wirklich weiter zu kommen. Ich verwende VC++2008.
Wenn vielleicht jemand einen sachdienlichen Hinweis liefern könnte, was zu dem genannten Verhalten führt, wäre ich sehr dankbar!
MFG
Andy
1
//aus Dialogklasse CTabDialog aufgerufen zur Übergabe Funkt.Pointer+this und Starten des Timers
Da das Timer-Konzept in Windows keine vernünftige Möglichkeit bietet,
Benutzerdaten (im konkreten Fall ein Pointer auf das CModulTime-Objekt)
an das Timer-Callback (hier TimerFunc) zu übertragen, scheint es gängige
Praxis zu sein, dafür das Timer-Event (zweites Argument in SetTimer und
drittes Argument in der Callback-Funktion) zu missbrauchen.
Das ist zwar gruselig, passt aber ganz gut zu Microsoft, denn die Doku
dazu und Windows insgesamt sind nicht weniger gruselig. So wird der
Benutzer über die Bedeutung der vier Argumente der Callback-Funktion
völlig im Unklaren gelassen:
Anmerkungen: Das & vor TimerFunc in deinem Code ist überflüssig, da
TimerFunc (ohne Argumentliste) bereits ein Pointer ist. Evtl. kann auch
das (TIMERPROC) weggelassen werden, da TimerFunc ja bereits die passende
Signatur hat und deswegen kein Cast erforderlich sein sollte.
Disclaimer: Ich kann nicht für die Richtigkeit des oben Geschriebenen
garantieren, dass ich das als Windows-Laie auch nur zusammengegoogelt
habe ;-)
Ich benutze hier keine Threads.
CModulTime ist auch keine MFC-Klasse, ich vermute, man spricht dann beim "Timereinsatz" von der WIN-API, korrekt? Verglichen mit dem Timerhandling innerhalb der MFC via WM_ONTIMER erscheint mir das alles auch etwas kompliziert bzw. nicht gut dokumentiert...
Muß hier aber nochmal erwähnen, daß ich absolut kein Programmierexperte bin.
Ich schaue mir verschiedene Codeschnipsel im Netz zu einem Thema an, versuche die zu verstehen und dann in meinen Code einzubauen.
Der Hinweis von Yalu jedenfalls ist schon recht zutreffend.
Die Win32-API-Funktion SetTimer() kann für alle möglichen Dinge verwendet werden, neben ihrer eigentlichen Aufgabe, Windows-Messages an irgendwelche Fenster zu senden, kann sie auch eine Callbackfunktion aufrufen.
Wenn diese Funktion eine Memberfunktion einer C++-Klasse ist, /muss/ diese Funktion als /static/ deklariert werden, denn sie wird ohne impliziten This-Pointer aufgerufen.
Damit man aus dieser statischen Memberfunktion heraus wiederum andere Memberfunktionen der zugehörigen Klasse heraus aufrufen kann, muss ein This-Pointer herbeigeschafft werden, der auf die zugehörige Objektinstanz verweist.
Daher /muss/ der Win32-API-Funktion der This-Pointer der Objektinstanz als opaker Parameter übergeben werden, damit der Callbackmechanismus beim Aufruf der Callbackfunktion diesen Parameter auch entsprechend weiterreicht.
Wenn nicht mit Windows-Fensterfunktionen gearbeitet wird, ist diese Funktion insgesamt eher sehr ungeeignet, um das Ziel zu erreichen, da sind die Multimedia-Timer-Funktionen deutlich besser geeignet, da sie außerhalb des Messagehandlings funktionieren.
Kern ist hier timeSetEvent() und hier insbesondere der Parameter dwUser (der als der eben beschriebene opake Pointer missbraucht werden kann). Dieser Parameter ist auch in der Funktionssignatur der verwendeten Callbackfunktion (LPTIMECALLBACK) beschrieben.
Wenn aber die entsprechende Anwendung eine GUI-Oberfläche hat, mithin also Fensterfunktionen und Messagehandler verwendet, ist es wenig sinnvoll, mit Timer-Callbacks zu arbeiten. Da ist der seit über dreißig Jahren etablierte Weg mit WM_TIMER der deutlich bessere.
Ich schaue mir verschiedene Codeschnipsel im Netz zu einem Thema an,
versuche die zu verstehen und dann in meinen Code einzubauen.
Ich halte das für keinen zielführenden Weg, da diese Codeschnipsel oft völlig unterschiedliche Programmierkonzepte vermischen und so nur unnötige Verwirrung und Komplexität schaffen.
Die Memberfunktion muss nicht static sein, das wird ja von der fp Klasse behandelt und die bekommt im Konstruktor auch die Instanz mit.
Im verlinkten Thread ist im letzten Post die Alternative mit der C++ Standard Lib gezeigt, das würde ich unter Windows eher verwenden. Aber es wird nicht das Problem lösen.
Die Memberfunktion muss nicht static sein, das wird ja von der fp Klasse
behandelt und die bekommt im Konstruktor auch die Instanz mit.
Hier nicht, denn SetTimer() weiß nichts davon. Der wird ein Funktionspointer übergeben, und der wird ohne this-Pointer aufgerufen, und damit muss die hier verwendete Funktion /static/ sein.
Was da im oben zu sehenden Codeschnipsel mit "fp" veranstaltet wird, hat mit dem verwendeten Callback-Mechanismus nichts zu tun.
Wie ich schon schrieb, wenn man irgendwelche irgendwo im Internet gefundenen Codefragmente zusammenkopiert, ist das seltenst zielführend.
Dann ist mir noch aufgefallen, daß der Timerhandler "TestCallBackToTabDgViaTimer" aufgerufen wird, egal welcher der 4 Parameter der "TimerFunc" gecastet wird. Steht in allen vieren der gleiche Zahlenwert (=Adresse)?
Steht in allen vieren der
gleiche Zahlenwert (=Adresse)?
Die Adresse von TestCallBackToTabDgViaTimer steht nicht im Objekt, auf das der This-Pointer verweist.
Das wäre überflüssig; denn wenn Du zwei Instanzen der Klasse anlegst, bleibt die Funktion, die aufgerufen wird, dieselbe, nur die /Daten/, die zur Objektinstanz gehören (d.h. etwaige Membervariablen) unterscheiden sich.
Dein Beispielcode knallt nur deswegen nicht, weil TestCallBackToTabDgViaTimer den this-Pointer nicht derefenziert.
Ich nehme stark an, daß "fp" keine Membervariable ist, sondern irgendwo außerhalb Deines Objektes existiert.
Die Doku zu SetTimer sagt für deinen Fall, am besten so aufrufen:
SetTimer(NULL, 0, [elapseTime], [Timerproc]);
Timerproc muß die Signatur void Timerproc(HWND,UINT,UINT_PTR,DWORD)
haben, also ein static Member wäre ok.
Es wird angeraten, die Exceptionunterdrückung auszuschalten:
"UOI_TIMERPROC_EXCEPTION_SUPPRESSION flag to false through the SetUserObjectInformationW function, otherwise the application could behave unpredictably and could be vulnerable to security exploits. For more info, see SetUserObjectInformationW."
CModuleTime::TimerFunc muss aber schon static sein, sonst würde der Compiler meckern. Und fp ist Instanz einer speziellen FunctionPointer Klasse, kein nackter Pointer, also auch ok.
Wenn Beep() ausgeführt wird ist es ja soweit ok, nur das GUI mag es nicht. Wird in den Debugausgaben etwas gemeldet?
Was heißt eigentlich "funktioniert nicht" genau? Wird eine Fehlermeldung
ausgegeben, hängt sich das Programm auf, oder sagt eine Stimme im
Hintergrund "Ätsch, funktioniert nicht"?
Wenn du nach fp.call() einen weiteren Beep()-Aufruf (am besten mit
anderer Frequenz) einfügst, wird dieser dann ausgeführt? Falls nicht,
bedeutet das, dass das Programm in fp.call() steckenbleibt.
Dann ist mir noch aufgefallen, daß der Timerhandler
"TestCallBackToTabDgViaTimer" aufgerufen wird, egal welcher der 4
Parameter der "TimerFunc" gecastet wird. Steht in allen vieren der
gleiche Zahlenwert (=Adresse)?
Für den bloßen Aufruf von Beep() wird die Objektadresse gar nicht
benötigt, deswegen funktioniert es bis zu diesem Punkt auch mit einer
falschen Adresse.
Sie wird aber für fp.call() benötigt, falls fp eine nichtstatische
Member-Variable von CModulTime ist.
Du solltest den Wert von unnamedParam3 mal ausgeben. Wenn ich
die MS-Logik richtig verstanden habe, sollte er der Adresse des
CModulTime-Objekts entsprechen, die du als zweites Argument an
SetTimer() übergeben hast.
In deiner ursprünglichen Version hast du statt der Objektadresse den
Wert 123 an SetTimer() übergeben, dessen Dereferenzierung natürlich ein
unvorhersehbares Verhalten erklären würde. Deswegen dachte ich, dass
mein obiger Vorschlag eigentlich gar nicht so schlecht sein sollte ;-)
Was heißt eigentlich "funktioniert nicht" genau? Wird eine Fehlermeldung
ausgegeben, hängt sich das Programm auf, oder sagt eine Stimme im
Hintergrund "Ätsch, funktioniert nicht"?
Das Programm stürzt nicht ab, die Methode CTabDialog::*CallBackFunctionTabDlg wird nur nicht aufgerufen (gebe dort AfxMessageBox testweise aus)
Wenn du nach fp.call() einen weiteren Beep()-Aufruf (am besten mit
anderer Frequenz) einfügst, wird dieser dann ausgeführt? Falls nicht,
bedeutet das, dass das Programm in fp.call() steckenbleibt.
Genau das hatte ich schon gemacht, der zweite Beep() danach kommt nicht mehr, d.h. das fp.Call() bleibt stecken.
Du solltest den Wert von unnamedParam3 mal ausgeben. Wenn ich
die MS-Logik richtig verstanden habe, sollte er der Adresse des
CModulTime-Objekts entsprechen, die du als zweites Argument an
SetTimer() übergeben hast.
this Zeiger an SetTimer 2.Argument übergeben 0x2f4ef10
die unnamedParam1 bis 4 in der Timerfunktion: 0, 0x113, 0x6ce9, 0x2a16729b
Die Adresse des this-Zeigers sehe ich nicht, Kommentar siehe unten.
Mit jeder Ausführung des Timerhandlers gibt es folgenden Fehler
Eine Ausnahme (erste Chance) bei 0x00b37776 in MyBus.exe: 0xC0000005: Zugriffsverletzung beim Lesen an Position 0x00006cf1.
Offensichtlich enthält der 3.Parameter (bzw. alle 4) keine gültige Adresse. Hatte jetzt eigentlich die Adresse des this-Zeigers erwartet...
Ich habe das unter Wine erfolgreich getestet (Code s. Anhang). Um nicht
die Mbed-Bibliothek herunterladen zu müssen, habe ich std::function
anstelle von mbed::FunctionPointer verwendet.
was für eine schwere Geburt - jetzt funktioniert es endlich!
Wird das gültige Handle im 1.Argument von SetTimer übergeben, bekomme ich im 3 Parameter der Timerfunktion den this-Zeiger auch zurück. :-)
Zusammenfassend kann man sagen - es fehlten 2 Dinge:
Beim Aufruf von SetTimer
im ersten Parameter ein gültiges Handle des übergeordneten Dialogs
im zweiten Parameter der this-Zeiger der aufrufenden Klasse
Danke an alle, die sich bei der Problemlösung beteiligt haben, besonders
natürlich Yalu für das Nachvollziehen! Ich denke, sein Codebeispiel ist sehr nützlich für jeden, der erstmalig so etwas umsetzen will/muss.