Game Development Community

dev|Pro Game Development Curriculum

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:
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_CACHE
as 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!

#1
09/13/2011 (10:50 am)
Many thanks for sharing this, bank! (And Rene!) I think this solves one of my rare crashes. :) Awesome!
#2
09/27/2011 (11:17 am)
This is very useful. Thanks, bank, for another great resource.
#3
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
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
11/25/2011 (5:33 pm)
I couldn't get it to compile without commenting out

case 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
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.