tutorials.de Buch-Aktion 05/2012
  • C/C++/ASM Verkettung von Funktionen mit variabler Parameterliste

    Wer kennt es nicht ? Man arbeitet mit STL strings (std::string) und braucht jedoch Merkmale der printf() Funktionen wie z.B. Substitutionen wie %s oder %i.
    Ist an sich keine schwere Aufgabe, man könnte einen normalen c-style String anlegen, sprintf() benutzen und diesen hinterher einem std::string-Container übergeben.
    Doch sähe etwas wie

    Code cpp:
    1
    
    std::string testString = FormatString("Hallo %s!", "Andy");

    nicht sehr viel ansprechender aus, als

    Code cpp:
    1
    2
    3
    
    char testString[1024] = {0};
    sprintf(testString, "Hallo %s!", "Andy");
    std::string sndString = testString;

    ?

    Man könnte anstatt dieses Tutorial zu lesen natürlich auch zu Libraries greifen, wie z.b. boost::format. Doch mag es vorkommen, dass man sein Programm klein halten muss, und da die Jungs von Boost nicht gerade halbe Sachen machen, wäre dies keine Option.

    Das Ziel ist nun also, eine Funktion mit dem Prototypen

    Code cpp:
    1
    
    std::string FormatString(const char* InString, ...);

    zu erstellen, welche sich die printf() Familie zu nutze macht.
    Ich werde davon ausgehen, dass der Leser Vorkenntnisse zur Handhabung von variabler Parameterliste und Assembler hat.
    Desweiteren wurde FormatString für Microsoft Visual C/C++ geschrieben. Ich gebe daher keine Garantie oder Hilfestellung, ob und wie es mit anderen Compilern funktioniert!

    Nun denn, mögen wir beginnen.

    Schritt 1:

    Wir müssen ermitteln, wie viele Parameter nun eigentlich übergeben wurden. Wir können natürlich einen weiteren Parameter an FormatString übergeben, doch können wir die Anzahl ebenfalls anhand von InString und der printf() Substitutionssyntax automatisch ermitteln:

    Code cpp:
    1
    2
    3
    4
    5
    6
    7
    8
    
    size_t  args = 0;
     
    for(size_t i = 0; InString[i] != 0; i++){
        if(InString[i] == '%'){                 // Wir halten Ausschau nach jedem Vorkommen von %,
            if(InString[i+1] == '%')    i++;        // dann überprüfen wir, ob es sich um %% handelt, wofür kein Parameter benötigt ist
            else                args++;     // und wenn nicht, bedeutet das, wir müssten einen Parameter mehr erwarten!
        }
    }

    Schritt 2:

    Zuallererst müssen wir dazu erst einmal verdeutlichen, wie die Macros für variable Parameterlisten (in MSVC: va_list / va_start / va_arg / va_end ) agieren und wie dagegen die Parameterübergabe an Funktionen festgelegt wurde.

    va_start erstellt einen Zeiger, welcher einen Parameter nach dem im Macro Übergebenen auf dem Stack zeigt, woraufhin dann
    va_arg jeweils per Aufruf diesen Zeiger (siehe Unten) von links nach rechts verschiebt! (An Alle, die meine Wortwahl kommentieren möchten: Ich weiß ! Ich wählte diese Worte um die bildliche Vorstellung eines Anfängers, welcher noch gar nicht weiß, was ein Zeiger tatsächlich ist, zu fördern )

    Dagegen werden jedoch in Assembler standardmäßig die Parameter vor dem Funktionsaufruf von rechts nach links auf den Stack geschoben!
    (Da ich Assemblergrundkenntnisse vorausgesetzt habe, werde ich darauf nicht weiter eingehen)

    Code :
    1
    2
    3
    4
    5
    
        Macro's:    Links [I]->[/I] Rechts
     
    void IrgendEineFunktion(int Parameter 1, int Parameter 2, int Parameter 3 [....])
     
        ASM:        Links [I]<-[/I] Rechts

    Damit wir nun nicht jeden Parameter erst zwischenspeichern müssen, wäre es klüger die Macro's umzuschreiben, sodass sie also per Aufruf von Rechts nach Links zeigen, beginnend ab dem letzten Parameter.
    Dazu jedoch erst einmal ein zusammenfassender Auszug aus vadefs.h:

    Code :
    1
    2
    3
    4
    5
    6
    
    #ifdef __cplusplus && _M_IX86
    #   define _ADDRESSOF(v)        ( &reinterpret_cast<const char &>(v) )
    #   define _INTSIZEOF(n)        ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
    #   define _crt_va_start(ap,v)      ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
    #   define _crt_va_arg(ap,t)        ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    #endif

    Von Interesse sind _crt_va_start und _crt_va_arg. Ohne tiefer in die Materie zu gehen und das Band zu sprengen, werde ich einfach meine umgeschriebenen Macro's anhängen:

    Code :
    1
    2
    3
    4
    5
    6
    
    #   if defined(_M_IX86)
    #       define  _ab_va_start(ap,v,a)    ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) * a )
    #       define  _ab_va_arg(ap,t)    ( *(t *)((ap -= _INTSIZEOF(t)) + _INTSIZEOF(t)) )
    #   elif
    #       error Dieses Tutorial behandelt nur x86 CPU
    #   endif

    Schritt 3:

    Nun müssen wir uns an den Aufruf einer printf() Funktion machen. Ich habe mich für _snprintf() entschieden.

    Code cpp:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    char    OutString[1024] = {0};
     
    if(_args){
        va_list     vl;
        DWORD       arg;
     
        _ab_va_start(vl, InString, args);
     
        for(size_t i = 0; i < args; i++){
            arg = _ab_va_arg(vl, DWORD);
            __asm push arg
        }
     
        DWORD       pInStr      = PtrToUlong(InString),
                pOutStr     = PtrToUlong(OutString),
                pCall       = PtrToUlong(&_snprintf);
     
        __asm {
            push    pInStr
            push    1023
            push    pOutStr
            call    pCall
            mov eax, args
            imul    eax, 4
            add eax, 12
            add esp, eax
        }
        
        va_end(vl);
    }

    Ich denke mit Kenntnissen über variablen Parameterlisten und ASM sollte der dieser Teil klar verständlich sein, sollte dem jedoch nicht so sein:

    In der for-Schleife werden alle variablen Parameter von FormatString in den Buffer "arg" gespeichert und daraufhin auf den Stack geschoben (push). Dabei spielt es keine Rolle, welcher Datentyp genommen wird, solange er 32bit groß ist. long (DWORD) passt daher gut ins Profil.
    Daraufhin werden die restlichen Parameter für _snprintf() auf den Stack geschoben, wobei man sehr klar sehen kann, dass dies von Rechts nach Links passiert.
    Nach dem eigentlichen Aufruf von _snprintf() wird ESP korrigiert und das ganze mit - dem in diesem Fall unnötigen - va_end beendet!

    Die ganze Funktion sähe daher so aus:

    Code cpp:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    #ifndef _WINDOWS_
    #   include <windows.h>
    #endif
     
    #ifndef _STRING_
    #   include <string>
    #endif
     
    #if defined(_M_IX86)
    #   define  _ab_va_start(ap,v,a)    ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) * a )
    #   define  _ab_va_arg(ap,t)    ( *(t *)((ap -= _INTSIZEOF(t)) + _INTSIZEOF(t)) )
    #elif
    #   error Dieses Tutorial behandelt nur x86 CPU
    #endif
     
    std::string FormatString(const char * InString, ...){
        if(!InString)
            return "";
     
        size_t  args = 0;
     
        for(size_t i = 0; InString[i] != 0; i++){
            if(InString[i] == '%'){                 // Wir halten Ausschau nach jedem Vorkommen von %,
                if(InString[i+1] == '%')    i++;        // dann überprüfen wir, ob es sich um %% handelt, wofür kein Parameter benötigt ist
                else                args++;     // und wenn nicht, bedeutet das, wir müssten einen Parameter mehr erwarten!
            }
        }   
     
        char    OutString[1024] = {0};
     
        if(_args){
            va_list     vl;
            DWORD       arg;
        
            _ab_va_start(vl, InString, args);
        
            for(size_t i = 0; i < args; i++){
                arg = _ab_va_arg(vl, DWORD);
                __asm push arg
            }
        
            DWORD       pInStr      = PtrToUlong(InString),
                    pOutStr     = PtrToUlong(OutString),
                    pCall       = PtrToUlong(&_snprintf);
        
            __asm {
                push    pInStr
                push    1023
                push    pOutStr
                call    pCall
                mov eax, args
                imul    eax, 4
                add eax, 12
                add esp, eax
            }
     
            va_end(vl);
        }
     
        return OutString;
    }


    Wenn man möchte, kann man die Funktion für Unicode auch noch weitgehend templateisieren - auch wenn eine if-Abfrage für die richtige printf() Funktion nun nicht mehr wirklich ein allgemeines Template ist.

    Code cpp:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    template <typename _C>
    std::basic_string<_C, std::char_traits<_C>, std::allocator<_C> >    StringFormat (const _C * string, ...){
        _C  __buf[1024] = {0};
     
        if(!string)
            return __buf;
     
        size_t  __args      = 0;
     
        for(size_t i = 0; string[i] != (_C)'\0'; i++){
            if(string[i] == (_C)'%'){
                if(string[i+1] == (_C)'%')  i++;
                else                __args++;
            }
        }
     
        if(__args){
            va_list     vl;
            DWORD       __arg;
     
            _ab_va_start(vl, string, __args);
     
            for(size_t i = 0; i < __args; i++){
                __arg = _ab_va_arg(vl, DWORD);
                __asm push __arg
            }
     
            DWORD           pstr        = PtrToUlong(string),
                        pbuf        = PtrToUlong(__buf),
                        pcall       = (sizeof(_C) == 1) ? PtrToUlong(&_snprintf) : PtrToUlong(&_snwprintf);
     
            __asm {
                push    pstr
                push    1023
                push    pbuf
                call    pcall
                mov eax, __args
                imul    eax, 4
                add eax, 12
                add esp, eax
            }
     
            va_end(vl);
        }
     
        return __buf;
    }


    Aufrufen kann man dann mit

    Code cpp:
    1
    2
    
    std::string testStringA = FormatString<char>("Guten %s, %s!", "Tag", "Andy");
    std::wstring testStringW = FormatString<wchar_t>(L"Und die %i. Variante für Unicode", 2);

    oder ggf. Macros bzw (wenn man weiß wie) typedefs



    ENDE

    Ich hoffe das Tutorial war in einer oder mehrer Hinsicht nützlich für Euch.

    MfG AnbriX
     


    Kommentare 2 Kommentare
    1. Avatar von OnlyFoo
      OnlyFoo -
      Wieso verwendest du nicht einfach gleich die vsprintf-Funktion, die die variable Anzahl an argumenten selbst handhabt? Link dazu: http://www.cplusplus.com/reference/c...tdio/vsprintf/
    1. Avatar von Matthias Reitinger
      Matthias Reitinger -
      Das habe ich mich auch gefragt… je nach Anwendungsfall würde ich bei C++ aber sowieso auf *sprintf verzichten und stattdessen String streams verwenden.

      Grüße,
      Matthias
    Kommentare Kommentar schreiben

    Klicke hier, um dich anzumelden

    Wie nennt man ein vierbeiniges Tier, das bellen kann?