Thread-safe console for Torque 3D
by Fyodor -bank- Osokin · 09/13/2011 (5:01 am) · 8 comments
This resources makes the console sub-system to be thread-safe in Torque 3D.
Same can be applied to other engines (TGEA/T2D).
Engine/source/core/strings/stringFunctions.cpp:
Inside:
Next, StringTable!
Engine/source/core/strings/stringTable.h:
Inside
Engine/source/core/strings/stringTable.cpp:
Change the head of insert() so it look like this (adding lock of mutex):
Open
Engine/source/core/strings/unicode.cpp:
And disable define:
The console itself:
Engine/source/console/console.cpp:
At the top of file, after includes, add:
Fixing _printf:
Add include at top of same file:
Change _printf() from
change from:
After the printf() function, add:
Now, the fixed functions:
Now it's even safe to call the following in a thread:
Thanks to Rene for helping on parts of this!
Same can be applied to other engines (TGEA/T2D).
Engine/source/core/strings/stringFunctions.cpp:
Inside:
S32 dSscanf(const char *buffer, const char *format, ...)change
static void* sVarArgs[20];to:
void* sVarArgs[20];(here we are removing "static", as statics are bad idea to use in threads!).
Next, StringTable!
Engine/source/core/strings/stringTable.h:
Inside
class _StringTable:Find
//..skipped StringTableEntry _EmptyString; protected: static const U32 csm_stInitSize;and add Mutex declaration:
//..skipped StringTableEntry _EmptyString; Mutex mMutex; protected: static const U32 csm_stInitSize;
Engine/source/core/strings/stringTable.cpp:
Change the head of insert() so it look like this (adding lock of mutex):
StringTableEntry _StringTable::insert(const char* _val, const bool caseSens)
{
MutexHandle mutex;
mutex.lock( &mMutex, true );
// Added 3/29/2007 -- If this is undesirable behavior, let me know -patw
const char *val = _val;
if( val == NULL )
val = "";Same for lookup():StringTableEntry _StringTable::lookup(const char* val, const bool caseSens)
{
MutexHandle mutex;
mutex.lock( &mMutex, true );
Node **walk, *temp;
U32 key = hashString(val);and lookupn():StringTableEntry _StringTable::lookupn(const char* val, S32 len, const bool caseSens)
{
MutexHandle mutex;
mutex.lock( &mMutex, true );
Node **walk, *temp;
U32 key = hashStringn(val, len);Open
Engine/source/core/strings/unicode.cpp:
And disable define:
//#define TORQUE_ENABLE_UTF16_CACHEas it doesn't play nice in multi-threaded environment.
The console itself:
Engine/source/console/console.cpp:
At the top of file, after includes, add:
static Mutex* sLogMutex;Add mutex at the init():
void init()
{
AssertFatal(active == false, "Con::init should only be called once.");
// Set up general init values.
active = true;
logFileName = NULL;
newLogFile = true;
gWarnUndefinedScriptVariables = false;
sLogMutex = new Mutex;Don't forget to clean-up:void shutdown()
{
AssertFatal(active == true, "Con::shutdown should only be called once.");
active = false;
smConsoleInput.remove(postConsoleInput);
consoleLogFile.close();
Namespace::shutdown();
AbstractClassRep::shutdown();
Compiler::freeConsoleParserList();
SAFE_DELETE( sLogMutex );
}The actual lock:static void log(const char *string)
{
// Lock.
MutexHandle mutex;
if( sLogMutex )
mutex.lock( sLogMutex, true );
// Bail if we ain't logging.
if (!consoleLogMode)
{
return;
}Fixing _printf:
Add include at top of same file:
#include "console/simEvents.h"
Change _printf() from
static void _printf(ConsoleLogEntry::Level level, ConsoleLogEntry::Type type, const char* fmt, va_list argptr)to:
static void _printf(ConsoleLogEntry::Level level, ConsoleLogEntry::Type type, const char* fmt)Down inside function:
change from:
dVsprintf(buffer + offset, sizeof(buffer) - offset, fmt, argptr);to:
dSprintf(buffer + offset, sizeof(buffer) - offset, "%s", fmt);
After the printf() function, add:
class ConPrinfThreadedEvent : public SimEvent
{
ConsoleLogEntry::Level mLevel;
ConsoleLogEntry::Type mType;
char *mBuf;
public:
ConPrinfThreadedEvent(ConsoleLogEntry::Level level = ConsoleLogEntry::Normal, ConsoleLogEntry::Type type = ConsoleLogEntry::General, const char *buf = NULL)
{
mLevel = level;
mType = type;
if(buf)
{
mBuf = (char*)dMalloc(dStrlen(buf)+1);
dMemcpy((void*)mBuf, (void*)buf, dStrlen(buf));
mBuf[dStrlen(buf)] = 0;
}
else
mBuf = NULL;
}
~ConPrinfThreadedEvent()
{
SAFE_FREE(mBuf);
}
virtual void process(SimObject *object)
{
if(mBuf)
{
switch(mLevel)
{
case ConsoleLogEntry::Normal :
Con::printf(mBuf);
break;
case ConsoleLogEntry::Warning :
Con::warnf(mType, mBuf);
break;
case ConsoleLogEntry::Error :
Con::errorf(mType, mBuf);
break;
case ConsoleLogEntry::Hack :
Con::hackf(mType, mBuf);
break;
}
}
}
};Now, the fixed functions:
void printf(ConsoleLogEntry::Type type, const char* fmt,...)
{
va_list argptr;
va_start(argptr, fmt);
char buf[8192];
dVsprintf(buf, sizeof(buf), fmt, argptr);
if(!ThreadManager::isMainThread())
Sim::postEvent(Sim::getRootGroup(), new ConPrinfThreadedEvent(ConsoleLogEntry::Normal, type, buf), Sim::getTargetTime());
else
_printf(ConsoleLogEntry::Normal, type, buf);
va_end(argptr);
}
void warnf(ConsoleLogEntry::Type type, const char* fmt,...)
{
va_list argptr;
va_start(argptr, fmt);
char buf[8192];
dVsprintf(buf, sizeof(buf), fmt, argptr);
if(!ThreadManager::isMainThread())
Sim::postEvent(Sim::getRootGroup(), new ConPrinfThreadedEvent(ConsoleLogEntry::Warning, type, buf), Sim::getTargetTime());
else
_printf(ConsoleLogEntry::Warning, type, buf);
va_end(argptr);
}
void errorf(ConsoleLogEntry::Type type, const char* fmt,...)
{
va_list argptr;
va_start(argptr, fmt);
char buf[8192];
dVsprintf(buf, sizeof(buf), fmt, argptr);
if(!ThreadManager::isMainThread())
Sim::postEvent(Sim::getRootGroup(), new ConPrinfThreadedEvent(ConsoleLogEntry::Error, type, buf), Sim::getTargetTime());
else
_printf(ConsoleLogEntry::Error, type, buf);
va_end(argptr);
}
void printf(const char* fmt,...)
{
va_list argptr;
va_start(argptr, fmt);
char buf[8192];
dVsprintf(buf, sizeof(buf), fmt, argptr);
if(!ThreadManager::isMainThread())
Sim::postEvent(Sim::getRootGroup(), new ConPrinfThreadedEvent(ConsoleLogEntry::Normal, ConsoleLogEntry::General, buf), Sim::getTargetTime());
else
_printf(ConsoleLogEntry::Normal, ConsoleLogEntry::General, buf);
va_end(argptr);
}
void warnf(const char* fmt,...)
{
va_list argptr;
va_start(argptr, fmt);
char buf[8192];
dVsprintf(buf, sizeof(buf), fmt, argptr);
if(!ThreadManager::isMainThread())
Sim::postEvent(Sim::getRootGroup(), new ConPrinfThreadedEvent(ConsoleLogEntry::Warning, ConsoleLogEntry::General, buf), Sim::getTargetTime());
else
_printf(ConsoleLogEntry::Warning, ConsoleLogEntry::General, buf);
va_end(argptr);
}
void errorf(const char* fmt,...)
{
va_list argptr;
va_start(argptr, fmt);
char buf[8192];
dVsprintf(buf, sizeof(buf), fmt, argptr);
if(!ThreadManager::isMainThread())
Sim::postEvent(Sim::getRootGroup(), new ConPrinfThreadedEvent(ConsoleLogEntry::Error, ConsoleLogEntry::General, buf), Sim::getTargetTime());
else
_printf(ConsoleLogEntry::Error, ConsoleLogEntry::General, buf);
va_end(argptr);
}Now it's even safe to call the following in a thread:
Con::printf("This one is called from a separate thread! Woo-hoo!! It's easier to debug stuff now!");Thanks to Rene for helping on parts of this!
About the author
Game developer.
#2
09/27/2011 (11:17 am)
This is very useful. Thanks, bank, for another great resource.
#3
how can do in TGEA+AFX+MMOKIT?
and I very hope this all can release in T3D 1.2...
09/27/2011 (11:43 pm)
S32 dSscanf(const char *buffer, const char *format, ...)how can do in TGEA+AFX+MMOKIT?
and I very hope this all can release in T3D 1.2...
#4
Two minor corrections (I applied this to a fresh "FPS Example" for Torque 3D 1.1):
stringTable.h/cpp is in Engine/source/core/ not Engine/source/core/strings/
And I needed to add the following to the includes of stringTable.h:
Thanks!
10/02/2011 (6:31 am)
Nice resource!Two minor corrections (I applied this to a fresh "FPS Example" for Torque 3D 1.1):
stringTable.h/cpp is in Engine/source/core/ not Engine/source/core/strings/
And I needed to add the following to the includes of stringTable.h:
#ifndef _PLATFORM_THREADS_MUTEX_H_ #include "platform/threads/mutex.h" #endif
Thanks!
#5
in Torque 1.2
Is this entry still needed somewhere?
11/25/2011 (5:33 pm)
I couldn't get it to compile without commenting outcase ConsoleLogEntry::Hack :
Con::hackf(mType, mBuf);
break;in Torque 1.2
Is this entry still needed somewhere?
#6
12/07/2011 (5:01 pm)
@Dion: I ran into the same problem; I can only assume that's something custom in his engine build...
#7
The StringTable will add a new string to the table if it does not exist, but fetch one if it already does. It is case-insensitive by default.
I ran into an interesting situation. We create DSO-s to offer some basic protection for our client side scripts against tampering and then pack those files into gap files that we patch for every new version. All dso-s are compiled in one run through assembling a list of script files that need to be processed.
One job of the compiler is to fetch strings from the script source - variable names for example - and to store them (in the string table as they are of StringTableEntry type).
The interesting part is that all the inserts happen when they happen - there's no way to tell the order of insertions for the strings nor to reproduce it in the same order since this is threaded. What I realized was that sometimes the same string is found, but with different case - ie. %fileName and %filename. The string table will treat it as one and store the first one in a compiled dso.
This is fine unless you count on a dso being deterministic. Ie. if you want to use it to patch a previous dso or you want to package the files and patch that file - you get it. Since the two dso-s will be different - despite the fact that they run the same code - in such a case a patch would be created without the need.
I guess not many people are affected by this problem, but I just thought I'd mention it, I spent some time tracking this down. I am still not 100% sure whether this resource has anything to do with this at all because the compiling process is (partially) running in parallel as well. I just found it interesting, and if you were going to compile to dso-s and then create binary difference patches, then you might want to look out for this.
I am still exploring what a good workaround would be. Probably changing all inserts to be case sensitive in CMDscan.l.
Sorry for the rant, I'm hoping I can save someone most of the "adventure" of hunting this one down. :)
07/30/2012 (2:20 pm)
I found something interesting. Not a bug, more like an.. inconsistency.The StringTable will add a new string to the table if it does not exist, but fetch one if it already does. It is case-insensitive by default.
I ran into an interesting situation. We create DSO-s to offer some basic protection for our client side scripts against tampering and then pack those files into gap files that we patch for every new version. All dso-s are compiled in one run through assembling a list of script files that need to be processed.
One job of the compiler is to fetch strings from the script source - variable names for example - and to store them (in the string table as they are of StringTableEntry type).
The interesting part is that all the inserts happen when they happen - there's no way to tell the order of insertions for the strings nor to reproduce it in the same order since this is threaded. What I realized was that sometimes the same string is found, but with different case - ie. %fileName and %filename. The string table will treat it as one and store the first one in a compiled dso.
This is fine unless you count on a dso being deterministic. Ie. if you want to use it to patch a previous dso or you want to package the files and patch that file - you get it. Since the two dso-s will be different - despite the fact that they run the same code - in such a case a patch would be created without the need.
I guess not many people are affected by this problem, but I just thought I'd mention it, I spent some time tracking this down. I am still not 100% sure whether this resource has anything to do with this at all because the compiling process is (partially) running in parallel as well. I just found it interesting, and if you were going to compile to dso-s and then create binary difference patches, then you might want to look out for this.
I am still exploring what a good workaround would be. Probably changing all inserts to be case sensitive in CMDscan.l.
Sorry for the rant, I'm hoping I can save someone most of the "adventure" of hunting this one down. :)
#8
03/30/2014 (5:10 pm)
Thanks for the detailed run-down, Konrad! I think it's probably an unavoidable issue of having a case-insensitive scripting language. If we were to make StringTable inserts case sensitive, then the whole scripting language would become case sensitive, since string comparison would become case sensitive. 
Associate Konrad Kiss
Bitgap Games