716 lines
17 KiB
C
716 lines
17 KiB
C
/* generate wowit.h and wowit.c from wow.it
|
|
*
|
|
* 20-Feb-1997 DaveHart created
|
|
*/
|
|
|
|
#include <time.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <io.h>
|
|
#include <sys\stat.h>
|
|
#include <fcntl.h>
|
|
#include <windows.h>
|
|
|
|
|
|
VOID ErrorAbort(PSZ pszMsg);
|
|
BYTE GetReturnOpcode(PSZ *ppsz);
|
|
PSZ GetApiName(PSZ *ppsz);
|
|
PSZ GetApi32Name(PSZ *ppsz, PSZ pszApi16);
|
|
BYTE GetArgOpcode(PSZ *ppsz);
|
|
PSZ GetOpcodeName(BYTE bInstr);
|
|
PSZ GetLine(PSZ pszBuf, int cbBuf, FILE *fp);
|
|
VOID ReadTypeNames(FILE *fIn, PSZ szTypesPrefix, PSZ *OpcodeNamesArray, int *pnOpcodeNames);
|
|
PSZ DateTimeString(VOID);
|
|
|
|
#define IS_RET_OPCODE(b) (b & 0x80)
|
|
|
|
#define MAX_IT_INSTR 16
|
|
|
|
typedef struct tagITINSTR {
|
|
int cbInstr;
|
|
int offSwamp;
|
|
BYTE Instr[MAX_IT_INSTR];
|
|
} ITINSTR;
|
|
|
|
#define MAX_INSTR_TABLE_SIZE 512
|
|
ITINSTR InstrTable[MAX_INSTR_TABLE_SIZE];
|
|
int iNextInstrSlot = 0;
|
|
|
|
typedef struct tagTHUNKTABLESLOT {
|
|
PSZ pszAPI;
|
|
PSZ pszAPI32; // if Win32 routine name doesn't match pszAPI
|
|
int iInstrSlot;
|
|
int cbInstr; // how much of this slot we're using
|
|
} THUNKTABLESLOT;
|
|
|
|
#define MAX_THUNK_TABLE_SIZE 1024
|
|
THUNKTABLESLOT ThunkTable[MAX_THUNK_TABLE_SIZE];
|
|
int iNextThunkSlot = 0;
|
|
|
|
#define MAX_ARG_OPCODE_NAMES 32
|
|
PSZ ArgOpcodeNames[MAX_ARG_OPCODE_NAMES];
|
|
int nArgOpcodeNames;
|
|
|
|
#define MAX_RET_OPCODE_NAMES 32
|
|
PSZ RetOpcodeNames[MAX_RET_OPCODE_NAMES];
|
|
int nRetOpcodeNames;
|
|
|
|
static char szArgumentTypes[] = "Argument Types:";
|
|
static char szReturnTypes[] = "Return Types:";
|
|
|
|
int __cdecl main(int argc, char **argv)
|
|
{
|
|
FILE *fIn, *fOutH, *fOutC;
|
|
char szBuf[256], szOff1[32], szOff2[32];
|
|
PSZ psz, pszAPI, pszAPI32;
|
|
ITINSTR ThisInstr;
|
|
BYTE bRetInstr;
|
|
BYTE *pbInstr;
|
|
int i, iSwampOffset;
|
|
int iMaxArgs = 0;
|
|
int cbDiff;
|
|
|
|
if (argc != 2) {
|
|
ErrorAbort("Usage:\n genwowit <inputfile>\n");
|
|
}
|
|
|
|
if (!(fIn = fopen(argv[1], "rt"))) {
|
|
ErrorAbort("Unable to open input file\n");
|
|
}
|
|
|
|
//
|
|
// The input file (wow.it) uses # to begin comment lines.
|
|
// Aside from comments, it must begin with two special lines
|
|
// to define the available type names for arguments and
|
|
// function return values.
|
|
//
|
|
// They look like:
|
|
//
|
|
// Argument Types: WORD, INT, DWORD, LPDWORD, PTR, PTRORATOM, HGDI, HUSER, COLOR, HINST, HICON, POINT, 16ONLY, 32ONLY;
|
|
// Return Types: DWORD, WORD, INT, HGDI, HUSER, ZERO, HICON, ONE, HPRNDWP;
|
|
//
|
|
// Read these lines into the ArgOpcodeNames and RetOpcodeNames arrays.
|
|
//
|
|
|
|
ReadTypeNames(fIn, szArgumentTypes, ArgOpcodeNames, &nArgOpcodeNames);
|
|
ReadTypeNames(fIn, szReturnTypes, RetOpcodeNames, &nRetOpcodeNames);
|
|
|
|
//
|
|
// Each input line in the main part has a very restricted syntax:
|
|
//
|
|
// RETTYPE Api16[=Api32](TYPE1, TYPE2, ... TYPEn); # comment
|
|
//
|
|
// If Api32 isn't specified it's the same as Api16.
|
|
// The types come from the set above only.
|
|
//
|
|
// Actually everything following the ) is ignored now.
|
|
//
|
|
|
|
while (GetLine(szBuf, sizeof szBuf, fIn)) {
|
|
|
|
psz = szBuf;
|
|
|
|
//
|
|
// Pick up the return type, space-delimited
|
|
//
|
|
|
|
bRetInstr = GetReturnOpcode(&psz);
|
|
|
|
//
|
|
// Pick up the API name, leaving psz pointing past the open-paren
|
|
//
|
|
|
|
pszAPI = GetApiName(&psz);
|
|
|
|
//
|
|
// Pick up the 32-bit name if it exists
|
|
//
|
|
|
|
pszAPI32 = GetApi32Name(&psz, pszAPI);
|
|
|
|
//
|
|
// Pick up the arg types into Instr array
|
|
//
|
|
|
|
memset(&ThisInstr, 0, sizeof ThisInstr);
|
|
pbInstr = ThisInstr.Instr;
|
|
|
|
while (*psz && *psz != ')') {
|
|
*pbInstr++ = GetArgOpcode(&psz);
|
|
}
|
|
|
|
//
|
|
// Keep track of the max used args
|
|
//
|
|
|
|
iMaxArgs = max(iMaxArgs, (pbInstr - ThisInstr.Instr));
|
|
|
|
//
|
|
// Tack on the return opcode
|
|
//
|
|
|
|
*pbInstr++ = bRetInstr;
|
|
|
|
//
|
|
// Record instruction bytes used for this one.
|
|
//
|
|
|
|
ThisInstr.cbInstr = (pbInstr - ThisInstr.Instr);
|
|
|
|
//
|
|
// Make sure we haven't overrun
|
|
//
|
|
|
|
if ( ThisInstr.cbInstr > MAX_IT_INSTR ) {
|
|
printf("Thunk for %s too many args (%d) increase MAX_IT_INSTR beyond %d.\n",
|
|
pszAPI, ThisInstr.cbInstr, MAX_IT_INSTR);
|
|
ErrorAbort("Increase MAX_IT_INSTR in intthunk.h\n");
|
|
}
|
|
|
|
//
|
|
// Now we have a fully-formed opcode stream, see if we can pack it
|
|
// in with any previously recorded ones. Walk through the table
|
|
// from the start looking for any entry which already contains this
|
|
// opcode sequence (possibly as part of a longer sequence) or which
|
|
// is itself contained by this opcode sequence. If we find one,
|
|
// change it to be the longer sequence if needed and use it. We'll
|
|
// distinguish later between the multiple uses using the cbInstr in
|
|
// each thunk table entry. The logic here assumes the matches will
|
|
// always be at the end, since ret opcodes always have 0x80 bit set
|
|
// and no others do, and each sequence ends with one.
|
|
//
|
|
|
|
for (i = 0; i < iNextInstrSlot; i++) {
|
|
//if (0 == memcmp(Instr, InstrTable[i], sizeof Instr)) {
|
|
// break;
|
|
//}
|
|
|
|
//
|
|
// Is ThisInstr a subsequence of this table entry?
|
|
//
|
|
|
|
if (ThisInstr.cbInstr <= InstrTable[i].cbInstr &&
|
|
0 == memcmp(ThisInstr.Instr,
|
|
InstrTable[i].Instr + (InstrTable[i].cbInstr -
|
|
ThisInstr.cbInstr),
|
|
ThisInstr.cbInstr)) {
|
|
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Is this table entry a subsequence of ThisInstr?
|
|
//
|
|
|
|
if (InstrTable[i].cbInstr < ThisInstr.cbInstr &&
|
|
0 == memcmp(InstrTable[i].Instr,
|
|
ThisInstr.Instr + (ThisInstr.cbInstr -
|
|
InstrTable[i].cbInstr),
|
|
InstrTable[i].cbInstr)) {
|
|
|
|
//
|
|
// Blast the longer ThisInstr over the existing shorter
|
|
// instruction.
|
|
//
|
|
|
|
memcpy(&InstrTable[i], &ThisInstr, sizeof InstrTable[i]);
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Check the next instruction table entry.
|
|
//
|
|
}
|
|
|
|
//
|
|
// If we didn't find a match, add to the end.
|
|
//
|
|
|
|
if (i == iNextInstrSlot) {
|
|
memcpy(&InstrTable[i], &ThisInstr, sizeof InstrTable[i]);
|
|
iNextInstrSlot++;
|
|
|
|
if (iNextInstrSlot == MAX_INSTR_TABLE_SIZE) {
|
|
ErrorAbort("Increase MAX_INSTR_TABLE_SIZE in genwowit.c\n");
|
|
}
|
|
}
|
|
|
|
//
|
|
// Add this one to the thunk table.
|
|
//
|
|
|
|
ThunkTable[iNextThunkSlot].pszAPI = pszAPI;
|
|
ThunkTable[iNextThunkSlot].pszAPI32 = pszAPI32;
|
|
ThunkTable[iNextThunkSlot].iInstrSlot = i;
|
|
ThunkTable[iNextThunkSlot].cbInstr = ThisInstr.cbInstr;
|
|
iNextThunkSlot++;
|
|
|
|
if (iNextThunkSlot == MAX_THUNK_TABLE_SIZE) {
|
|
ErrorAbort("Increase MAX_THUNK_TABLE_SIZE in genwowit.c\n");
|
|
}
|
|
}
|
|
|
|
fclose(fIn);
|
|
|
|
//
|
|
// Now we're ready to output the results.
|
|
//
|
|
|
|
if (!(fOutH = fopen("wowit.h", "wt"))) {
|
|
ErrorAbort("Cannot open wowit.h output file\n");
|
|
}
|
|
|
|
fprintf(fOutH,
|
|
"//\n"
|
|
"// DO NOT EDIT.\n"
|
|
"//\n"
|
|
"// wowit.h generated by genwowit.exe from wow.it on\n"
|
|
"//\n"
|
|
"// %s\n"
|
|
"//\n\n", DateTimeString());
|
|
|
|
fprintf(fOutH, "#include \"intthunk.h\"\n\n");
|
|
|
|
fprintf(fOutH, "#define MAX_IT_ARGS %d\n\n", iMaxArgs);
|
|
|
|
//
|
|
// Spit out the two types of opcode manifests.
|
|
//
|
|
|
|
for (i = 0; i < nArgOpcodeNames; i++) {
|
|
fprintf(fOutH, "#define IT_%-20s ( (UCHAR) 0x%x )\n", ArgOpcodeNames[i], i);
|
|
}
|
|
|
|
fprintf(fOutH, "\n#define IT_RETMASK ( (UCHAR) 0x80 )\n");
|
|
|
|
for (i = 0; i < nRetOpcodeNames; i++) {
|
|
sprintf(szBuf, "%sRET", RetOpcodeNames[i]);
|
|
fprintf(fOutH, "#define IT_%-20s ( IT_RETMASK | (UCHAR) 0x%x )\n", szBuf, i);
|
|
}
|
|
|
|
fprintf(fOutH, "\n");
|
|
|
|
//
|
|
// ITID_ manifests map an API name to its slot
|
|
// in the thunk table. Each one looks like:
|
|
//
|
|
// #define ITID_ApiName 0
|
|
//
|
|
|
|
for (i = 0; i < iNextThunkSlot; i++) {
|
|
fprintf(fOutH, "#define ITID_%-40s %d\n", ThunkTable[i].pszAPI, i);
|
|
}
|
|
|
|
fprintf(fOutH, "\n#define ITID_MAX %d\n", i-1);
|
|
|
|
fclose(fOutH);
|
|
|
|
|
|
//
|
|
// wowit.c has two tables, the instruction table and
|
|
// the thunk table.
|
|
//
|
|
|
|
if (!(fOutC = fopen("wowit.c", "wt"))) {
|
|
ErrorAbort("Cannot open wowit.c output file\n");
|
|
}
|
|
|
|
fprintf(fOutC,
|
|
"//\n"
|
|
"// DO NOT EDIT.\n"
|
|
"//\n"
|
|
"// wowit.c generated by genwowit.exe from wow.it on\n"
|
|
"//\n"
|
|
"// %s\n"
|
|
"//\n\n", DateTimeString());
|
|
|
|
|
|
fprintf(fOutC, "#include \"precomp.h\"\n");
|
|
fprintf(fOutC, "#pragma hdrstop\n");
|
|
fprintf(fOutC, "#define WOWIT_C\n");
|
|
fprintf(fOutC, "#include \"wowit.h\"\n\n");
|
|
|
|
//
|
|
// Spit out the instruction table, packing bytes in the process
|
|
// and filling in the aoffInstrTable array with offsets for each
|
|
// entry in this program's InstrTable. Those offsets are used
|
|
// in writing the final thunk table.
|
|
//
|
|
|
|
iSwampOffset = 0;
|
|
|
|
fprintf(fOutC, "CONST BYTE InstrSwamp[] = {\n");
|
|
|
|
for (i = 0; i < iNextInstrSlot; i++) {
|
|
|
|
fprintf(fOutC, " /* %3d 0x%-3x */ ", i, iSwampOffset);
|
|
|
|
pbInstr = InstrTable[i].Instr;
|
|
InstrTable[i].offSwamp = iSwampOffset;
|
|
|
|
do {
|
|
fprintf(fOutC, "%s, ", GetOpcodeName(*pbInstr));
|
|
iSwampOffset++;
|
|
} while (!IS_RET_OPCODE(*pbInstr++));
|
|
|
|
fprintf(fOutC, "\n");
|
|
|
|
}
|
|
|
|
fprintf(fOutC, "};\n\n");
|
|
|
|
fprintf(fOutC, "CONST INT_THUNK_TABLEENTRY IntThunkTable[] = {\n");
|
|
|
|
for (i = 0; i < iNextThunkSlot; i++) {
|
|
|
|
//
|
|
// Concatenate the API name followed by a comma into
|
|
// szBuf, so the combination can be left-justified in the output.
|
|
//
|
|
|
|
sprintf(szBuf, "%s,", ThunkTable[i].pszAPI32);
|
|
|
|
//
|
|
// cbDiff is the offset into the instruction stream where
|
|
// this thunks instruction stream begins.
|
|
//
|
|
|
|
cbDiff = InstrTable[ ThunkTable[i].iInstrSlot ].cbInstr -
|
|
ThunkTable[i].cbInstr;
|
|
|
|
//
|
|
// Format the swamp offset so it can be left-justified in the output.
|
|
//
|
|
|
|
sprintf(szOff1, "%x",
|
|
InstrTable[ ThunkTable[i].iInstrSlot ].offSwamp + cbDiff);
|
|
|
|
//
|
|
// If this thunk table entry will point past the start of
|
|
// an instruction (because of sharing), format the offset
|
|
// past the start of the instruction into szOff2
|
|
//
|
|
|
|
if (cbDiff) {
|
|
sprintf(szOff2, "+ %d ", cbDiff);
|
|
} else {
|
|
szOff2[0] = '\0';
|
|
}
|
|
|
|
fprintf(fOutC,
|
|
" /* %3d */ { (FARPROC) %-32s InstrSwamp + 0x%-4s }, /* %d %s*/ \n",
|
|
i,
|
|
szBuf,
|
|
szOff1,
|
|
ThunkTable[i].iInstrSlot,
|
|
szOff2);
|
|
}
|
|
|
|
fprintf(fOutC, "};\n\n");
|
|
|
|
fclose(fOutC);
|
|
|
|
printf("Generated wowit.h and wowit.c from wow.it\n"
|
|
"%d thunks, %d unique instruction streams, %d instruction bytes, %d max args.\n",
|
|
iNextThunkSlot, iNextInstrSlot, iSwampOffset, iMaxArgs);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
BYTE GetReturnOpcode(PSZ *ppsz)
|
|
{
|
|
int i;
|
|
char szBuf[32];
|
|
PSZ psz;
|
|
|
|
//
|
|
// Copy the name up to the first space to szBuf,
|
|
// then skip any remaining spaces leaving caller's
|
|
// pointer pointing at API name.
|
|
//
|
|
|
|
psz = szBuf;
|
|
while (**ppsz != ' ') {
|
|
*psz++ = *((*ppsz)++);
|
|
};
|
|
|
|
*psz = 0;
|
|
|
|
while (**ppsz == ' ') {
|
|
(*ppsz)++;
|
|
};
|
|
|
|
i = 0;
|
|
while (i < nRetOpcodeNames &&
|
|
strcmp(szBuf, RetOpcodeNames[i])) {
|
|
i++;
|
|
}
|
|
|
|
if (i == nRetOpcodeNames) {
|
|
printf("%s is not a valid return type.\n", szBuf);
|
|
ErrorAbort("Invalid return type.\n");
|
|
}
|
|
|
|
return (BYTE)i | 0x80;
|
|
}
|
|
|
|
|
|
|
|
PSZ GetApiName(PSZ *ppsz)
|
|
{
|
|
char szBuf[128];
|
|
PSZ psz;
|
|
|
|
//
|
|
// Copy the name up to the first space or open-paren or equals sign
|
|
// to szBuf, then skip any remaining spaces and open-parens leaving caller's
|
|
// pointer pointing at first arg type or equals sign
|
|
//
|
|
|
|
psz = szBuf;
|
|
while (**ppsz != ' ' && **ppsz != '(' && **ppsz != '=') {
|
|
*psz++ = *((*ppsz)++);
|
|
};
|
|
|
|
*psz = 0;
|
|
|
|
while (**ppsz == ' ' || **ppsz == '(') {
|
|
(*ppsz)++;
|
|
};
|
|
|
|
if (!strlen(szBuf)) {
|
|
ErrorAbort("Empty API name\n");
|
|
}
|
|
|
|
return _strdup(szBuf);
|
|
}
|
|
|
|
|
|
|
|
PSZ GetApi32Name(PSZ *ppsz, PSZ pszApi16)
|
|
{
|
|
char szBuf[128];
|
|
PSZ psz;
|
|
|
|
if (**ppsz != '=') {
|
|
return pszApi16;
|
|
}
|
|
|
|
(*ppsz)++; // skip =
|
|
|
|
//
|
|
// Copy the name up to the first space or open-paren
|
|
// to szBuf, then skip any remaining spaces and open-parens leaving caller's
|
|
// pointer pointing at first arg type
|
|
//
|
|
|
|
psz = szBuf;
|
|
while (**ppsz != ' ' && **ppsz != '(') {
|
|
*psz++ = *((*ppsz)++);
|
|
};
|
|
|
|
*psz = 0;
|
|
|
|
while (**ppsz == ' ' || **ppsz == '(') {
|
|
(*ppsz)++;
|
|
};
|
|
|
|
if (!strlen(szBuf)) {
|
|
ErrorAbort("Empty API32 name\n");
|
|
}
|
|
|
|
return _strdup(szBuf);
|
|
}
|
|
|
|
|
|
|
|
|
|
BYTE GetArgOpcode(PSZ *ppsz)
|
|
{
|
|
char szBuf[32];
|
|
PSZ psz;
|
|
int i;
|
|
|
|
//
|
|
// Copy the name up to the first space or comma close-paren
|
|
// to szBuf, then skip any remaining spaces and commas,
|
|
// leaving caller's pointer pointing at next arg type
|
|
// or close-paren.
|
|
//
|
|
|
|
psz = szBuf;
|
|
while (**ppsz != ' ' && **ppsz != ',' && **ppsz != ')') {
|
|
*psz++ = *((*ppsz)++);
|
|
};
|
|
|
|
*psz = 0;
|
|
|
|
while (**ppsz == ' ' || **ppsz == ',') {
|
|
(*ppsz)++;
|
|
};
|
|
|
|
//
|
|
// szBuf has the type name, find it in the table.
|
|
//
|
|
|
|
i = 0;
|
|
while (i < nArgOpcodeNames &&
|
|
strcmp(szBuf, ArgOpcodeNames[i])) {
|
|
i++;
|
|
}
|
|
|
|
if (i == nArgOpcodeNames) {
|
|
printf("%s is not a valid arg type.\n", szBuf);
|
|
ErrorAbort("Invalid arg type.\n");
|
|
}
|
|
|
|
return (BYTE)i;
|
|
}
|
|
|
|
|
|
|
|
PSZ GetOpcodeName(BYTE bInstr)
|
|
{
|
|
char szBuf[64];
|
|
|
|
if (!IS_RET_OPCODE(bInstr)) {
|
|
sprintf(szBuf, "IT_%s", ArgOpcodeNames[bInstr]);
|
|
} else {
|
|
sprintf(szBuf, "IT_%sRET", RetOpcodeNames[bInstr & 0x7f]);
|
|
}
|
|
|
|
return _strdup(szBuf);
|
|
}
|
|
|
|
|
|
|
|
VOID ErrorAbort(PSZ pszMsg)
|
|
{
|
|
printf("GENWOWIT : fatal error GWI0001: Unable to process wow.it: %s\n", pszMsg);
|
|
exit(1);
|
|
}
|
|
|
|
|
|
//
|
|
// Read a line from the input file skipping
|
|
// comment lines with '#' in the first column.
|
|
//
|
|
|
|
PSZ GetLine(PSZ pszBuf, int cbBuf, FILE *fp)
|
|
{
|
|
do {
|
|
|
|
pszBuf = fgets(pszBuf, cbBuf, fp);
|
|
|
|
} while (pszBuf && '#' == *pszBuf);
|
|
|
|
return pszBuf;
|
|
}
|
|
|
|
|
|
//
|
|
// Read one of the two special lines at the start that
|
|
// define the available types.
|
|
//
|
|
|
|
|
|
VOID ReadTypeNames(FILE *fIn, PSZ pszTypesPrefix, PSZ *OpcodeNamesArray, int *pnOpcodeNames)
|
|
{
|
|
char chSave, szBuf[512];
|
|
PSZ psz, pszType;
|
|
|
|
if ( ! GetLine(szBuf, sizeof szBuf, fIn) ||
|
|
_memicmp(szBuf, pszTypesPrefix, strlen(pszTypesPrefix)) ) {
|
|
|
|
ErrorAbort("First line of input file must be 'Argument Types:', second 'Return Types:' ...\n");
|
|
}
|
|
|
|
psz = szBuf + strlen(pszTypesPrefix);
|
|
|
|
//
|
|
// Skip whitespace and commas
|
|
//
|
|
|
|
while (' ' == *psz || '\t' == *psz) {
|
|
psz++;
|
|
}
|
|
|
|
if ( ! *psz) {
|
|
ErrorAbort("No types found.\n");
|
|
}
|
|
|
|
do {
|
|
//
|
|
// Now we're looking at the first character of the type name.
|
|
//
|
|
|
|
pszType = psz;
|
|
|
|
//
|
|
// Find next whitespace, comma, semi, or null and turn it into a null.
|
|
// This turns this type name into a zero-terminated string.
|
|
//
|
|
|
|
while (*psz && ' ' != *psz && '\t' != *psz && ',' != *psz && ';' != *psz) {
|
|
psz++;
|
|
}
|
|
|
|
chSave = *psz;
|
|
*psz = 0;
|
|
|
|
OpcodeNamesArray[*pnOpcodeNames] = _strdup(pszType);
|
|
(*pnOpcodeNames)++;
|
|
|
|
*psz = chSave;
|
|
|
|
//
|
|
// Skip whitespace and commas
|
|
//
|
|
|
|
while (' ' == *psz || '\t' == *psz || ',' == *psz) {
|
|
psz++;
|
|
}
|
|
|
|
} while (*psz && ';' != *psz);
|
|
|
|
if ( ! *pnOpcodeNames) {
|
|
ErrorAbort("No types found.\n");
|
|
}
|
|
}
|
|
|
|
//
|
|
// Return a formatted date/time string for now.
|
|
// Only checks system time once so that wowit.c and wowit.h
|
|
// will have same date/time string.
|
|
//
|
|
|
|
PSZ DateTimeString(VOID)
|
|
{
|
|
static char sz[256];
|
|
static int fSetupAlready;
|
|
|
|
if (!fSetupAlready) {
|
|
time_t UnixTimeNow;
|
|
struct tm *ptmNow;
|
|
|
|
fSetupAlready = TRUE;
|
|
|
|
_tzset();
|
|
|
|
time(&UnixTimeNow);
|
|
|
|
ptmNow = localtime(&UnixTimeNow);
|
|
|
|
strftime(sz, sizeof sz, "%#c", ptmNow);
|
|
|
|
strcat(sz, " (");
|
|
strcat(sz, _strupr(_tzname[0])); // naughty me
|
|
strcat(sz, ")");
|
|
}
|
|
|
|
return sz;
|
|
}
|