Forum: Compiler & IDEs GCC cleanup-Funktion / longjmp


von STM32MP1 liebäugler (Gast)


Lesenswert?

Hallo,

ich verwende das cleanup-Attribut des gcc um eine Interruptsperre bei 
verlassen des Scopes einer (Sperr)Variable wieder aufzuheben. Soweit 
funktioniert das auch. Wenn ich aber in einer Unterfunktion einen 
longjump() aufrufe, wird die Sperre nicht aufgehoben, da der Scope der 
Variable vermeitlich noch nicht verlassen ist. Gibt es eine Möglichkeit 
dem gcc mitzuteilen, dass eine Funktion zum verlassen des Scopes führt? 
__attribute__((noreturn)) führt nicht zum gewünschten Ergebnis.

Gruß

von Dr. Sommer (Gast)


Lesenswert?

Mit Destruktoren und Exceptions (statt longjmp) in C++ geht das. 
Erfordert aber eine Menge Overhead. Ich würde longjmp soweit irgends 
möglich vermeiden, und Destruktoren statt Compiler spezifischer 
Attribute verwenden.

von STM32MP1 liebäugler (Gast)


Lesenswert?

C++ Exceptions sind für µC doch ziemlich fett (Du schreibst es ja). Ich 
habe mit setjmp/longjump eine einfache Exceptionbehandlung in C 
implementiert. Es geht mir darum, Fehler (nicht irgendwelche Zustände 
wie z.B. in Java geläufig) auf jeden Fall zu behandeln. Über 
Rückgabewerte ist das Ganze nur bedingt zuverlässig, da man ja weiss, 
dass die Funktion immer funktioniert und der Wert dann auch nicht zu 
prüfen ist ;-)
Und jetzt bin auch das Problem im Zusammenhang mit longjmp gestossen. 
Den Komfort/Sicherheit welcher durch die "cleanup-Locks" erreicht wird, 
möchte ich nicht wieder missen.

von Dr. Sommer (Gast)


Lesenswert?

Das Problem ist, dass man zum automatischen Bereinigen beim 
Herausspringen eine Menge Technik braucht - "personality functions", 
Exceptios Tabellen, Speicher Pools... Das gleiche was man halt auch bei 
Exceptions macht. Bei uC muss man da wohl in den sauren Apfel beißen und 
es händisch machen. Da hat man aber sowieso meistens keine derart tiefen 
Aufruf Stacks. Das Cleanup kann man mit Destruktoren oder teilweise auch 
Lambdas in C++ automatisieren, aber longjmp bringt einfach alles 
durcheinander...

von Markus F. (mfro)


Lesenswert?

Das ist m.E. ein rein logisches Problem, kein Problem der Sprache. Auch 
C++-Exceptions und/oder Destruktoren können das nur verstecken, nicht 
beheben.

Wenn Du einen Mechanismus hast, der lediglich bei einem (geordneten) 
Verlassen einer Routine einen globalen Status aufhebt, mußt Du bei einem 
ungeordneten Verlassen (longjmp) eben selbst dafür sorgen, daß sauber 
aufgeräumt wird.

Wenn man sich schon in diese dunklen Gefilde hinabbegeben hat: sowas 
kann m.E. ein guter Grund für ein goto sein. Springt man mit goto aus 
einem Scope (der in deinem Fall die Variable mit dem cleanup Attribut 
enthalten könnte), wird der Stack noch abgeräumt. Das Sprungziel des 
gotos könnte dann dein longjmp() sein.

Allein, mir schaudert ob der Vorstellung ...

von Dr. Sommer (Gast)


Lesenswert?

Markus F. schrieb:
> Auch C++-Exceptions und/oder Destruktoren können das nur verstecken,
> nicht beheben.
>
> Wenn Du einen Mechanismus hast, der lediglich bei einem (geordneten)
> Verlassen einer Routine einen globalen Status aufhebt, mußt Du bei einem
> ungeordneten Verlassen (longjmp) eben selbst dafür sorgen, daß sauber
> aufgeräumt wird.

Die Idee war ja, Exceptions statt longjmp zu nutzen. Die Grundidee ist 
artverwandt, aber der Compiler kann dies geordnet umsetzen und eben 
Destruktoren aufrufen. Nur leider ist mindestens die Implementation vom 
GCC+newlib nicht wirklich uC-tauglich. Es wäre interessant da etwas 
geeignetes zu implementieren, das wäre aber ein beträchtlicher Aufwand. 
Realistisch wäre es, longjmp wegzulassen, und mit Rückgabewerten 
(Fehlercodes) und Destruktoren zu arbeiten. Das C++ Attribut "nodiscard" 
oder die GCC Erweiterung __attribute__((warn_unused_result)) können da 
helfen Fehler zu vermeiden.

von Nop (Gast)


Lesenswert?

Innerhalb eines Blocks, wo Ressourcenallokation stattgefunden hat, setzt 
man grundsätzlich kein longjmp ein, das ist ein Antipattern.

von Markus F. (mfro)


Lesenswert?

Ich habe das so verstanden: der TO hat jetzt so was:
1
void cleanup(void *lock)
2
{
3
    *lock = unlocked;
4
}
5
6
void function_may_fail(void)
7
{
8
    void *global_lock __attribute__((cleanup));
9
    bool failed = false;
10
    
11
    *global_lock = locked;
12
13
    /* mach was */
14
15
    failed = ...
16
17
    if (failed) longjmp(jbf);
18
19
    ...
20
}

Wenn ein Fehler auftritt, der zum longjmp() führt, wird die 
cleanup()-Funktion nicht ausgeführt. Mein Vorschlag wäre so was:
1
void cleanup(void *lock)
2
{
3
    *lock = unlocked;
4
}
5
6
void function_may_fail(void)
7
{
8
    do {
9
        void *global_lock __attribute__((cleanup));
10
        bool failed = false;
11
    
12
        *global_lock = locked;
13
14
        /* mach was */
15
        failed = ...
16
17
        if (failed) goto fail;
18
19
        return;
20
    } while (false);    
21
22
fail:
23
    longjmp(jbf);
24
}

Das müsste (ich hab's nicht ausprobiert) m.E. wie gewünscht 
funktionieren. Ist nicht schön, aber das ist longjmp() nie...

von Dr. Sommer (Gast)


Lesenswert?

Die m.M.n saubersten Versionen wären so (es ging ja um 
Interrupt-Sperren):
1
class IrqLocker {
2
  public:
3
    IrqLocker () { __disable_irq (); }
4
    ~IrqLocker () { __enable_irq (); }
5
    IrqLocker (const IrqLocker&) = delete;
6
    IrqLocker (IrqLocker&&) = delete;
7
    IrqLocker& operator = (const IrqLocker&) = delete;
8
    IrqLocker& operator = (IrqLocker&&) = delete;
9
};
10
11
bool machwas ();
12
13
void function_may_fail(void)
14
{
15
    IrqLocker irqLock;
16
17
    bool failed = !machwas ();
18
    if (failed) throw std::runtime_error ("faild");
19
}
oder mit Lambdas
1
template <typename F>
2
auto irqLock (F&& f) -> decltype (std::forward<F> (f) ()) {
3
  __disable_irq ();
4
  auto res = std::forward<F> (f) ();
5
  __disable_irq ();
6
  return res;
7
}
8
9
void function_may_fail(void)
10
{
11
    irqLock ([&] () {
12
        bool failed = !machwas ();
13
        if (failed) throw std::runtime_error ("faild");
14
    }
15
}

Der Programmfluss ist hier ähnlich wie longjmp und ziemlich nah an dem, 
was ursprünglich gewünscht war, aber eben mit automatischem Aufräumen. 
Aber es ist standardisiert und braucht keine Compiler-spezifischen 
Attribute. Nur leider ist eben der Overhead dafür riesig. Daher würde 
ich die Fehlerbehandlung manuell machen:
1
class IrqLocker {
2
  public:
3
    IrqLocker () { __disable_irq (); }
4
    ~IrqLocker () { __enable_irq (); }
5
    IrqLocker (const IrqLocker&) = delete;
6
    IrqLocker (IrqLocker&&) = delete;
7
    IrqLocker& operator = (const IrqLocker&) = delete;
8
    IrqLocker& operator = (IrqLocker&&) = delete;
9
};
10
11
[[nodiscard]] bool machwas ();
12
13
[[nodiscard]] bool function_may_fail(void)
14
{
15
    IrqLocker irqLock;
16
17
    bool failed = !machwas ();
18
    if (failed) return false;
19
    ...
20
    return true;
21
}

Oder mit Lambdas:
1
template <typename F>
2
auto irqLock (F&& f) -> decltype (std::forward<F> (f) ()) {
3
  __disable_irq ();
4
  auto res = std::forward<F> (f) ();
5
  __disable_irq ();
6
  return res;
7
}
8
9
[[nodiscard]] bool machwas ();
10
11
[[nodiscard]] bool function_may_fail(void)
12
{
13
    return irqLock ([&] () {
14
15
        bool failed = !machwas ();
16
        if (failed) return false;
17
        ...
18
        return true;
19
    });
20
}

Mittels "nodiscard" sorgt man dafür, dass man die Auswertung der 
Rückgabe (Fehler-) Codes nicht vergisst. Die Interrupts werden immer 
automatisch ge-und entsperrt. Der Programmlauf ist anders als bei 
longjmp, es ist mehr Tipparbeit, aber dafür sauberer, lesbarer und auch 
standardisiert.

von DPA (Gast)


Lesenswert?

Markus F. schrieb:
> Das müsste (ich hab's nicht ausprobiert) m.E. wie gewünscht
> funktionieren. Ist nicht schön, aber das ist longjmp() nie...

Das kann man so aber schlecht machen, wenn das longjmp nicht noch in 
irgend einer Unterfunktion ist, und sonst braucht man das nicht 
wirklich. Würde ich sowas machen, würde ich das so irgendwie machen:
1
// utils.h
2
#ifndef UTILS_H
3
#define UTILS_H
4
5
#include <setjmp.h>
6
#include <string.h>
7
8
#ifndef UNPACK
9
#define UNPACK(A) A
10
#endif
11
12
#define AUTOCLEANUP_FUNC(N,X) \
13
  typedef struct { \
14
    jmp_buf ac_jmp; \
15
    UNPACK X \
16
  } ac_ ## N ##_t; \
17
  static inline void ac_ ## N ## _func_sub(ac_ ## N ##_t* ac); \
18
  void ac_ ## N ## _func(ac_ ## N ##_t* ac){ \
19
    memcpy(&ac_next_jmp,&ac->ac_jmp,sizeof(ac->ac_jmp)); \
20
    ac_ ## N ## _func_sub(ac); \
21
  } \
22
  static inline void ac_ ## N ## _func_sub(ac_ ## N ##_t* ac)
23
24
#define AUTOCLEANUP(N,ID) \
25
  __attribute__((cleanup(ac_ ## N ## _func))) ac_ ## N ##_t ID; \
26
  memset(&ID, 0, sizeof(ID)); \
27
  if(memcmp(&ac_next_jmp, &ac_unset_jmp, sizeof(ac_unset_jmp))){ \
28
    memcpy(&ID.ac_jmp, &ac_next_jmp, sizeof(ac_next_jmp)); \
29
    if(setjmp(ac_next_jmp)){ \
30
      ac_ ## N ## _func(&ID); \
31
      longjmp(ac_next_jmp, 1); \
32
    } \
33
  }
34
35
#define AUTOCLEANUP_FINAL_ENDPOINT if(setjmp(ac_next_jmp))
36
37
#define AUTOCLEANUP_BACKOUT \
38
  ((memcmp(&ac_next_jmp, &ac_unset_jmp, sizeof(ac_unset_jmp)) && (longjmp(ac_next_jmp,1),1)), 1)
39
40
extern jmp_buf ac_next_jmp;
41
extern const jmp_buf ac_unset_jmp;
42
43
#endif
44
45
// utils.c
46
jmp_buf ac_next_jmp;
47
const jmp_buf ac_unset_jmp;
48
49
// example.c
50
#include <stdio.h>
51
#include <stdlib.h>
52
53
AUTOCLEANUP_FUNC(test_cleanup, (
54
  int a;
55
  int b;
56
)){
57
  printf("test_cleanup:  a: %d  b: %d\n", ac->a, ac->b);
58
}
59
60
void bla(void){
61
  if(AUTOCLEANUP_BACKOUT)
62
    abort(); // There never was a AUTOCLEANUP_FINAL_ENDPOINT
63
}
64
65
void test(void){
66
  AUTOCLEANUP(test_cleanup, c)
67
  c.a = 1;
68
  c.b = 2;
69
  bla();
70
}
71
72
int main(){
73
  AUTOCLEANUP_FINAL_ENDPOINT {
74
    printf("Backout, bye!!!\n");
75
    return 1;
76
  }
77
  test();
78
  printf("Nooo, I want to say something!!!\n");
79
  return 0;
80
}

Hab das jetzt aber nicht allzu ausgiebig getestet.

Eventuell könnte auch 
https://github.com/guillermocalvo/exceptions4c#lightweight-version 
interessant sein.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.