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




Bereiche
Kategorien
Forum - Programming





tutorials.de-Systemmitteilung