Handmade Hero»Forums»Code
51 posts
Replacing sprintf
Edited by vexe on
Gettings!

I wanted to share with the community my sprintf replacement. It is not by all means a perfect one by it gets the job done at least for my needs. I wanted the usage to be exactly the same so I don't have to change the way I was doing things with sprintf. Here it goes:

[Code]
// Supports %s, %d, %c, %b and %f
u32 StringFormatVargs(string Buffer, string Input, va_list Args)
{
string Dst = Buffer;
u32 BytesWritten = 0;
u32 DefaultFractDigits = 3; // 3 digits after decimal point by default unless specified otherwise
u32 NextFractDigits = DefaultFractDigits;

while (*Input)
{
char c = *Input;
if (c == '%')
{
char Type = Input[1];
if (Type == 'c')
{
*Dst++ = va_arg(Args, char);
BytesWritten++;
}
else
{
if (IsNumeric(Type))
{
NextFractDigits = Type - 48;
Assert(Input[2] == 'f');
Type = 'f';
Input++;
}

string Replacement = 0;
string ToFree = 0;

switch (Type)
{
case 'b':
{
b32 Bool32 = va_arg(Args, b32);
Replacement = StringFromB32(Bool32);
} break;
case 's':
{
string String = va_arg(Args, string);
Replacement = String;
} break;
case 'd':
{
s32 Signed32 = va_arg(Args, s32);
Replacement = StringFromS32(Signed32);
ToFree = Replacement;
} break;
case 'f':
{
r32 Real32 = (r32)va_arg(Args, r64); // NOTE: float is promoted to double!
Replacement = StringFromR32(Real32, NextFractDigits);
NextFractDigits = DefaultFractDigits;
ToFree = Replacement;
} break;

default: InvalidSwitchCase;
}

while (*Replacement)
{
*Dst++ = *Replacement++;
BytesWritten++;
}

if (ToFree)
Free(ToFree);
}

Input++;
}
else
{
*Dst++ = c;
BytesWritten++;
}

Input++;
}

*Dst = 0;

return (BytesWritten);
}

u32 StringFormat(string Buffer, string Input, ...)
{
u32 BytesWritten;
va_list Args;
va_start(Args, Input);
BytesWritten = StringFormatVargs(Buffer, Input, Args);
va_end(Args);
return (BytesWritten);
}

[/Code]

Here are the auxiliary functions, I pulled them from some old code I had where I used to use 'int' instead of s32 etc.

[Code]

inline u32 GetDigitCount(s32 Value)
{
u32 Result;
for (Result = 1; (Value /= 10) > 0; Result++);
return Result;
}

void IntCpy(char *Dst, s32 Value, s32 Start, u32 Count)
{
s32 End = Start + Count;
for (s32 i = End - 1; i >= Start; i--, Value /= 10)
*(Dst + i) = (char)(Value % 10 + 48);
Dst[End] = 0;
}

string StringFromS32(s32 Value)
{
b32 Negative = Value < 0;
Value = Negative ? -Value : Value;
u32 NumDigits = GetDigitCount(Value);
string Result;
if (Negative)
{
Result = Salloc(NumDigits + 1);
char *Ptr = Result;
{
*Ptr = '-';
IntCpy(Ptr, Value, 1, NumDigits);
}
}
else
{
Result = Salloc(NumDigits);
char *ptr = Result;
IntCpy(ptr, Value, 0, NumDigits);
}
return Result;
}

string StringFromR32(r32 Value, u32 DecimalAccuracy)
{
// e.g. 3.148
b32 Negative = Value < 0;
if (Negative) Value = -Value;
int Mul = 1;
ForCount(DecimalAccuracy) Mul *= 10;
int Number = (int)(Value * Mul); // gets the number as a whole, e.g. 3148
int LeftNum = Number / Mul; // left part of the decimal point, e.g. 3
int RightNum = Number % Mul; // right part of the decimal pnt, e.g. 148
int LeftDigitCount = GetDigitCount(LeftNum); // e.g. 1
int RightDigitCount = GetDigitCount(RightNum); // e.g. 3
int Total = LeftDigitCount + RightDigitCount + 1; // +1 for '.'

string Result;
if (Negative)
{
Result = Salloc(Total + 1); // +1 for '-'
char *Ptr = Result;
{
*Ptr = '-';
IntCpy(Ptr, LeftNum, 1, LeftDigitCount);
*(Ptr + LeftDigitCount + 1) = '.';
IntCpy(Ptr, RightNum, LeftDigitCount + 2, RightDigitCount);
}
}
else
{
Result = Salloc(Total);
char *Ptr = Result;
{
IntCpy(Ptr, LeftNum, 0, LeftDigitCount);
*(Ptr + LeftDigitCount) = '.';
IntCpy(Ptr, RightNum, LeftDigitCount + 1, RightDigitCount);
}
}

return Result;
}

inline string StringFromB32(b32 Input)
{
return Input ? "true" : "false";
}

inline bool IsNumeric(char Input)
{
return Input >= '0' && Input <= '9';
}
[/Code]

Some typedefs and defines I use
[Code]
typedef char* string;
typedef signed int b32;
typedef signed int s32;
typedef unsigned int u32;
typedef float r32;
typedef double r64;

#define Salloc(N) (string)calloc(N + 1, sizeof(char))
#define ForCount(N) for(u32 i = 0; i < (N); i++)
[/Code]

The StringFormat function is using variable arguments defined in cstdargs but you could write your own va_list, va_etc equivalents quite easily.

I'm allocating memory for strings because I usually use that stuff for debugging code or metaprograms where I don't care much about it as long as it's doing what it's supposed to (so I don't care about security etc). I don't use that code in games. In my game I have the memory allocation stuff (Salloc etc) wrapped in macros to point to whatever memory allocation schemes I use (memory arena, heap alloc, etc) so I can easily change where these allocations get their memory from.

Usage:
[Code]
char Buffer[256];
StringFormat(Buffer, "Hello %s 1+1=%d, 2.10+2.75=%2f, (char)65=%c, it's %b", "World", 2, 4.85f, 'A', true);
[/Code]

Hope somebody finds this useful. Comments/suggestions/improvements/critique are all welcome!

Thanks!
elxenoanizd/vexe

EDIT: [Correction] Salloc should really allocate N + 1 instead of N to include the null terminator. And it's sizeof(char) not sizeof(string)
Bryan Taylor
55 posts
Replacing sprintf
A couple of tips:
a) return the number of bytes written to the buffer. That way, if you want to write multiple strings into the same buffer (i.e, to concatenate a log message) you know how much to offset the pointer by.

b) instead of implementing just sprintf, implement vsprintf (taking the va_list as an argument.) Then sprintf is just a wrapper around it. (The extra layer usually gets optimized out -- but then again if you're calling sprintf in hot code you have more problems than an extra function call.)

The advantage there is if you have some *other* function with variable args that wants to do printing, it can reuse vsprintf, whereas there's no way (that I know of, at least) to unpack the va_list into arguments for an sprintf call. (You can do this with a macro, using __VA_ARGS__, but anything else you want to do in that higher level *also* has to be part of the macro -- this gets nasty quick.)
51 posts
Replacing sprintf
a) good point! I usually get around that by doing sprintf(mystr + strlen(mystr), etc...); if I'm concatenating to mystr.

b) I actually tried this yesterday. It seems that you could pass a va_list to a '...' argument just fine. Used it in my platform layer log function:

[Code]

internal void
Win32Log(string Message, ...)
{
va_list Args;
va_start(Args, Message);
{
char Value[512];
StringFormat(Value, Message, Args);
OutputDebugString(Value);
}
va_end(Args);
}
[/Code]

Is that what you mean?
Mārtiņš Možeiko
2559 posts / 2 projects
Replacing sprintf
If StringFormat has va_list as third argument, then yes that is what he means. No need to pass va_list to ... argument. Just accept va_list directly. And make any other function as wrapper around StringFormat. Exactly what your Win32Log function does already.
51 posts
Replacing sprintf
Edited by vexe on
I take what I said previously back. If StringFormat takes '...' it doesn't mean that we could pass it a 'va_list'. I've modified it so it deals with a 'va_list' directly. And another wrapper version around it that takes '...' - it also returns the number of bytes written for concatenation purposes. So we can say:

[Code]
string(TestBuffer, 256);
u32 Written = StringFormat(TestBuffer, "This is a %s\n", "test");
Written += StringFormat(TestBuffer + Written, " Appending %d\n", Written);
Written += StringFormat(TestBuffer + Written, " Appending even more %d\n", Written);
Win32Log(TestBuffer); // now uses StringFormatVargs
[/Code]
Ameen Sayegh
51 posts
Replacing sprintf
you forgot to surround the N parameter with parenthesis in Salloc macro.
This might cause you a weird bug someday.