Callback funktioniert nicht aus Timer-Routine

OP #7788694
Lesenswert?

Hallo zusammen,

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
2
void CModulTime::StartModulTime(void* wnd, void (CTabDialog::*CallBackFunctionTabDlg)(void))
3
{
4
  fp.attach<CTabDialog>((CTabDialog*)wnd, CallBackFunctionTabDlg);  
5
  fp.call(); //###nur als Test, funktioniert!
6

7
  if(btimer_started==false)
8
    SetTimer(NULL, 123, 5000,(TIMERPROC) &TimerFunc);
9

10
  btimer_started = true;
11
}
12

13
//Timerfunktion, static
14
void CALLBACK CModulTime::TimerFunc(HWND unnamedParam1, UINT unnamedParam2, UINT_PTR unnamedParam3, DWORD unnamedParam4)
15
{  
16
    reinterpret_cast<CModulTime*>(unnamedParam3)->TestCallBackToTabDgViaTimer();
17
}
18

19
void CModulTime::TestCallBackToTabDgViaTimer()
20
{   
21
    Beep(500,100);//###wird ausgeführt
22
    fp.call(); //###funktioniert nicht! Warum?
23
}
24

25
//public, zum Testen aus CTabDialog aufgerufen
26
void CModulTime::TestCallBackToTabDgViaButton()
27
{   
28
    fp.call(); //###nur als Test, funktioniert!
29
}
Moderator #7788721
Lesenswert?

In Zeile 8 sollte wohl stehen:

1
SetTimer(NULL, reinterpret_cast<UINT_PTR>(this), 5000, (TIMERPROC)TimerFunc);

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:

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-timerproc

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 ;-)

J. S. schrieb:

Benutzt der Timer Threads? Dann funktioniert es nicht weil du nicht GUI Funktionen aus anderen Threads heraus aufrufen darfst.

Ja, das sollte auch noch geklärt werden. In der MS-Doku steht zwar nichts zu diesem Thema, aber das muss nichts heißen ;-)

OP #7788799
Lesenswert?

Hallo zusammen,

Danke für das Feedback!

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.

MFG Andy

#7788874
Lesenswert?

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.

Multimedia-Timer werden hier beschrieben:

https://learn.microsoft.com/en-us/windows/win32/multimedia/multimedia-timers

Andy W. schrieb:

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.

#7788905
Lesenswert?

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.

#7788978
Lesenswert?

J. S. schrieb:

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.

OP #7789008
Lesenswert?

So, habe den Timeraufruf angepasst.

1
SetTimer(NULL, reinterpret_cast<UINT_PTR>(this), 5000, TimerFunc);

Hat leider keine Änderung gebracht.

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)?

1
//Timerfunktion, static
2

3
void CALLBACK CModulTime::TimerFunc(HWND unnamedParam1, UINT unnamedParam2, UINT_PTR unnamedParam3, DWORD unnamedParam4)
4

5
{  
6

7
    reinterpret_cast<CModulTime*>(unnamedParam3)->TestCallBackToTabDgViaTimer();
8

9
}

Ich kann nachvollziehen, daß Ihr bei der Aufgabenstellung andere Wege empfehlt, das umzusetzen.

Ich wollte es halt nur verstehen. Wenn ich das nicht zum Laufen bekomme, muß ich eh versuchen, eine andere Lösung zu finden.

MFG Andy

#7789018
Lesenswert?

Andy W. schrieb:

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.

#7789028
Lesenswert?

Harald K. schrieb:

Dein Beispielcode knallt nur deswegen nicht, weil TestCallBackToTabDgViaTimer den this-Pointer nicht derefenziert.

Der Cast von unnamedParam3 löst eine Exception aus, die vom Windows-System abgefangen und ignoriert wird.

Ich weiß nicht warum du meinst das unnamedParam1 bis 4 irgendwas sinnvolles enthalten sollten.

1
void CALLBACK CModulTime::TimerFunc(HWND unnamedParam1, UINT unnamedParam2, UINT_PTR unnamedParam3, DWORD unnamedParam4)
2
{   reinterpret_cast<CModulTime*>(unnamedParam3)->TestCallBackToTabDgViaTimer();
3
}

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

Alles in https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer nachlesbar.

#7789048
Lesenswert?

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?

Moderator #7789049
Lesenswert?

Andy W. schrieb:

So, habe den Timeraufruf angepasst. Hat leider keine Änderung gebracht.

Schade.

Andy W. schrieb:

void CModulTime::TestCallBackToTabDgViaTimer() { Beep(500,100);//###wird ausgeführt fp.call(); //###funktioniert nicht! Warum? }

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 ;-)

OP #7789189
Lesenswert?

Das sind gute Fragen :-)

Yalu X. schrieb:

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)

Yalu X. schrieb:

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.

Yalu X. schrieb:

In deiner ursprünglichen Version hast du statt der Objektadresse den Wert 123 an SetTimer() übergeben

Das war mein Fehler, irgendwie hatte ich aus der MFC-Welt in Erinnerung, daß dort eine eindeutige ID für den Timer übergeben werden muss.

Yalu X. schrieb:

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.

J. S. schrieb:

Wird in den Debugausgaben etwas gemeldet?

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

Moderator #7789194
Lesenswert?

Yalu X. schrieb:

SetTimer(NULL, reinterpret_cast<UINT_PTR>(this), 5000, (TIMERPROC)TimerFunc);

Damit das so funktioniert wie gedacht, muss als erstes Argument ein Window-Handle übergeben werden (Microsoft will es so):

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer

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.

Angehängte Dateien:
OP #7789300
Lesenswert?

Hallo zusammen,

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

  1. im ersten Parameter ein gültiges Handle des übergeordneten Dialogs
  2. 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.

Euch einen schönen Start in die Woche!

MFG Andy

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren