FuBi: Automatic Function Exporting
for Scripting and Networking

Code Supplement

 

By Scott Bilas

Abstract

In this paper I present and discuss code used in Dungeon Siege’s FuBi. I had planned to write a fully functional sample application to demonstrate FuBi in action for scripting and networking (ready for GDC 2001 no less!), but I just don’t have time given the title I’m working on and the looming ship date.

The next best thing is to just upload a bunch of the code that I’ve written for Dungeon Siege, and that’s what this paper is about. Many of the important components of the system are included and discussed here. Unfortunately this means that this paper is far more complicated than my sample would have been, so hopefully you can follow it through all the extra clutter. If not, I’m always available for help via email. I’m putting this together in a big hurry as I find spare moments, so please bear with my poor writing and scatterbrained organization.

Important: this paper isn’t going to make a whole lot of sense without reading the FuBi paper on my web site first. Or you could start with my gem “A Generic Function Binding Interface”, also on my web site.

Utilities

First we need to cover some of the utilities I use globally in this code. This pair of macros is meant to be used in an enum. Use the first to start an enum and set some special constants for the given start number, and the second to get the count and end enum (for iteration).

#define SET_BEGIN_ENUM( x, num )        \

    x##BEFORE_BEGIN = num - 1

#define SET_END_ENUM( x )               \

    x##END,                             \

    x##BEGIN = x##BEFORE_BEGIN + 1,     \

    x##LAST  = x##END - 1,              \

    x##COUNT = x##END - x##BEGIN

 

A simple macro to wrap debug-only code:

#ifdef _DEBUG

#   define DEBUG_ONLY( f ) f

#else

#   define DEBUG_ONLY( f )

#endif

 

A nice helper to zero something out:

template <typename T> inline void ZeroObject( T& object )

    {  memset( &object, 0, sizeof( T ) );  }

 

This function checks to see if a pointer is pointing to “bad food”. Debug versions of memory managers typically use magic numbers like these for pre- and post-fill on memory blocks.

inline bool IsMemoryBadFood( DWORD d )

{

    return (    ( d == 0xDDDDDDDD )     // crt: dead land (deleted objects)

             || ( d == 0xCDCDCDCD )     // crt: clean land (new, uninit’d objects)

             || ( d == 0xFDFDFDFD )     // crt: no man's land (off the end)

             || ( d == 0xCCCCCCCC )     // vc++: stack objects init’d with this

             || ( d == 0xFEEEFEEE )     // ? nt internal ?

             || ( d == 0xBAADF00D ) );  // winnt: nt internal "not yours" filler

}

 

Here is a pair of classes that I use to wrap up the concept of a memory block – base pointer plus size. It’s safer than simply using a separate pointer and size parameter because the code can specifically look for this type being passed by value and adjust accordingly. Note that the class makes sure it doesn’t point to bad food as a simple safety, but doesn’t do anything more complicated than that (like calling IsBad[Read|Write]Ptr).

struct mem_ptr

{

    typedef void* MemType;

 

    void*  mem;         // pointer to start of memory

    size_t size;        // size of memory

 

    mem_ptr( void )               {  mem = NULL;  size = 0;

                                     DEBUG_ONLY( assert_valid() );  }

    mem_ptr( void* m, size_t s )  {  mem = m;  size = s;

                                     DEBUG_ONLY( assert_valid() );  }

 

    operator void* ( void ) const  {  return ( mem  );  }

    operator size_t( void ) const  {  return ( size );  }

 

    void assert_valid( void ) const

    {

        gpassert( !IsMemoryBadFood( (DWORD)mem ) );

    }

};

 

template <typename T> inline mem_ptr make_mem_ptr( T& obj )

    {  return ( mem_ptr( &obj, sizeof( T ) ) );  }

 

struct const_mem_ptr

{

    typedef const void* MemType;

 

    const void* mem;    // pointer to start of memory

    size_t      size;   // size of memory

 

    const_mem_ptr( void )                     {  mem = NULL;  size = 0}

    const_mem_ptr( const void* m, size_t s )  {  mem = m;  size = s;

                                                 DEBUG_ONLY( assert_valid() );  }

    const_mem_ptr( mem_ptr p )                {  mem = p;  size = p;

                                                 DEBUG_ONLY( assert_valid() );  }

 

    operator const void* ( void ) const  {  return ( mem  );  }

    operator size_t      ( void ) const  {  return ( size );  }

 

    void assert_valid( void ) const

    {

        gpassert( !IsMemoryBadFood( (DWORD)mem ) );

        gpassert( size < 1024 * 1024 * 1024 );

    }

};

 

template <typename T> inline const_mem_ptr make_const_mem_ptr( const T& obj )

    {  return ( const_mem_ptr( &obj, sizeof( T ) ) );  }

 

Damn C++ stupidly long keywords…

#define rcast reinterpret_cast

#define dcast dynamic_cast

#define scast static_cast

#define ccast const_cast

 

These macros are used to automatically define operators for enum types. I like to use certain enums as integers or bitfields and this makes it easier.

#define MAKE_ENUM_BIT_OPERATORS( x ) \

    inline x operator | ( x a, x b ) \

        {  return ( scast <x> ( scast <int> ( a ) | scast <int> ( b ) ) ); } \

    inline x operator & ( x a, x b ) \

        {  return ( scast <x> ( scast <int> ( a ) & scast <int> ( b ) ) ); } \

    inline x operator |= ( x& a, x b ) \

        {  return ( a = a | b ); } \

    inline x operator &= ( x& a, x b ) \

        {  return ( a = a & b ); } \

    inline x NOT( x a ) /* can’t figure out how to create an operator ~() */ \

        {  return ( scast <x> ( ~(scast <int> ( a )) ) ); }

 

#define MAKE_ENUM_MATH_OPERATORS( x ) \

    inline x operator ++ ( x& a )                    /* prefix increment */ \

        {  return ( a = scast <x> ( a + 1 ) );  } \

    inline x operator ++ ( x& a, int )               /* postfix increment */ \

        {  x old = a;  ++a;  return ( old );  } \

    inline x operator -- ( x& a )                    /* prefix decrement */ \

        {  return ( a = scast <x> ( a - 1 ) );  } \

    inline x operator -- ( x& a, int )               /* postfix decrement */ \

        {  x old = a;  --a;  return ( old );  } \

    inline x operator + ( x a, x b ) \

        {  return ( scast <x> ( scast <int> ( a ) + scast <int> ( b ) ) ); } \

    inline x operator - ( x a, x b ) \

        {  return ( scast <x> ( scast <int> ( a ) - scast <int> ( b ) ) ); } \

    inline x operator += ( x& a, x b ) \

        {  return ( a = scast <x> ( a + b ) ); } \

    inline x operator -= ( x& a, x b ) \

        {  return ( a = scast <x> ( a - b ) ); }

Simple Macros

This is the basic FuBi export macro. As in the paper, simply dllexport your functions by putting this in front of the function prototype.

#define FEX __declspec ( dllexport )

 

In our function importer we will look for functions prefixed with FUBI_ and treat them specially (this is the “protocol” I alluded to in the paper). These three macros set this up. Functions that we intend to be “reserved” we will wrap in the FUBI_RESERVED macro.

#define FEX_PREFIX FUBI_

#define FEX_PREFIX_TEXT #FEX_PREFIX

#define FUBI_RESERVED( NAME ) FEX_PREFIX##NAME

 

This macro is used to tag a class as a singleton, and classes that use this macro must have a static GetSingleton() function. See my gem in Game Programming Gems I called “An Automatic Singleton Utility” for an easy singleton class (also available on my web page). In the importer we will reserve a slot in the class’s type entry for a function pointer to its “get my singleton” method.

#define FUBI_SINGLETON_CLASS( T ) \

    FEX static T* __cdecl FUBI_RESERVED( GetClassSingleton )( void ) \

        {  return ( &GetSingleton() );  }

 

Simple classes that do not contain pointers or complex members are generally known as POD (plain old data) types. We can tag a class as a POD type by putting this macro inside of it. If a class exports this function, we will set the POD flag in the class and set its size as well. Any RPC (remote procedure call) code we have will need to pay attention to this flag and not pre/post-process accordingly.

#define FUBI_POD_CLASS( T ) \

    FEX static size_t __cdecl FUBI_RESERVED( PodGetSize )( void ) \

        {  return ( sizeof( T ) );  }

 

Here is how I tell a class that it is RPC capable via network cookie conversion. Put this at the bottom of your class declaration, passing in the name of the class and the names of functions that convert class pointers to/from network cookies. Note that this is unnecessary with POD types and singletons for obvious reasons. Typically this macro is used in classes that have ID’s that are globally consistent across machines. In the TO_FUNC function (which must take a T* and return a DWORD which is the network cookie) simply take the pointer and ask the class for its ID. In the FROM_FUNC function (which must take a DWORD network cookie and return a T*) look up the ID in the necessary database to resolve to a pointer.

#define FUBI_RPC_CLASS( T, TO_FUNC, FROM_FUNC ) \

    FEX static DWORD __cdecl FUBI_RESERVED( InstanceToNet )( T* instance ) \

        {  return ( TO_FUNC( instance ) );  } \

    FEX static T* __cdecl FUBI_RESERVED( NetToInstance )( DWORD cookie ) \

        {  return ( FROM_FUNC( cookie ) );  }

 

It just so happens that my scripting language (Siege)Skrit needed to know about inheritance of system types so it could have a pointer to a base and call a base function on it. This macro tells FuBi (a) when a class is derived from another and (b) what to offset the pointer by when casting to base. Detection of the function will give us the inheritance relationship, and calling the function will give us the offset.

#define FUBI_CLASS_INHERIT( T, BASE ) \

    FEX static int __cdecl FUBI_RESERVED( Inheritance )( BASE* ) \

            {  return ( (int)(BASE*)(T*)1 - (int)(T*)1 );  }

 

Now we need some constants. These are used internally in FuBi and in the network transport as virtual machine addresses. In Dungeon Siege, RPC_TO_LOCAL and RPC_TO_SERVER must resolve to actual machine ID’s for performance reasons, so they are not exactly “const”. Helper functions allow setting these after the network is up and running.

// $ this does not correspond to a DPID

const DWORD RPC_INVALID_ADDR        = (DWORD)( -1 );

// == DPNID_ALL_PLAYERS_GROUP

const DWORD RPC_TO_ALL              = (DWORD)(  0 );

// $ this does not correspond to a DPID

const DWORD RPC_TO_OTHERS           = (DWORD)( -2 );

// by default, each machine starts up as the server...

const DWORD RPC_TO_SERVER_DEFAULT   = (DWORD)( -3 );

// when joining an mp game, actual server changes the ID and gets the authority

const DWORD RPC_TO_LOCAL_DEFAULT    = (DWORD)( -3 );

// just a nice starting point for a non-dplay test app to assign machine id's

const DWORD RPC_TEST_ADDR_START     = (DWORD)(  1 );

 

// "constants" - treat as constant, modify ONLY by net pipe

extern const DWORD& RPC_TO_LOCAL;

extern const DWORD& RPC_TO_SERVER;

Advanced Macros

The RPC macro is a little crazy, and nested to make it easier to manage. As with all parts of FuBi, this macro was vastly simpler when originally written, but the needs of the project changed that over time and these macros just got more and more things added.

For example, I added the “embedded tag” work when I found out that detecting that a function is not RPC-capable is something that really needs to happen at startup time, rather than just-in-time. Because an exported function may be used for the scripting language or another purpose, you can’t assume a function which would fail to work for an RPC is actually intended to be used as an RPC (and give a bogus error). The only way to know for sure is to check to see if an RPC macro is used inside the function, and towards this end I wrote a scanner that would search for a special embedded tag right inside a function. If this tag was found, it’s safe to assume that the function is intended to be used for RPC’s, and then we can verify it at startup time to make sure it’s not passing weird pointers over the network etc.

Another enhancement came when Bart’s network layer came online and added the concept of “retries” (Bartosz Kijanka is our lead engineer). Dungeon Siege networking provides the usual guaranteed delivery stuff but adds a new concept called “guaranteed execution”. In our continuous world engine, just because a packet gets through doesn’t mean it can actually execute just yet. The object that a message is expected to operate on may not have entered the world yet. This is caused by differences in where the client and server think the world frustum center is, and is usually caused by lag or client machines being significantly slower than the server. While the client is catching up, processing of those packets must be deferred. In order to make this work, certain RPC’s are tagged as “retry” exports, which the receiver will periodically retry. The return value is a “retry cookie” that is used by the network transport to decide whether or not it succeeded or failed, or needs retrying. This cookie is detected by the FuBi importer and is handled specially by the dispatcher.

Anyway here are the macros. The last four are the important ones, and everything else is support. The FUBI_RPC_CALL_IMPL macro does all the real work. One of its critical roles is to look up the function that it’s in. As in the paper, this is done by looking up eip in the function database to see which one we’re in. Note that it is necessary to pass in the name of the function for RPC lookup because the linker will collapse multiple functions with identical code down to the same entry point. This is common with empty or other stub type functions. The name is necessary to differentiate the proper function.

// helper for param skipping - 4 bytes each

#define FUBI_PARAM_SKIP( COUNT ) \

    paramStart = rcast <BYTE*> ( paramStart ) + ((COUNT) * 4);

#define FUBI_NO_PARAM_SKIP \

    ;

 

// how far to search for RPC tag - this is actually 0x27 or so but give a

// little bit more for safety.

const int FUBI_RPC_MAX_SEARCH = 0x40;

 

// divider

#define FUBI_EMBED_TAG_MOV 0xB8      /* mov eax, [dword constant] */

 

// basic embedded tags

#define FUBI_EMBED_TAG_0 0x46        /* 'F' */

#define FUBI_EMBED_TAG_1 0x75        /* 'u' */

#define FUBI_EMBED_TAG_2 0x62        /* 'b' */

#define FUBI_EMBED_TAG_3 0x69        /* 'i' */

 

// special RPC tags

#define FUBI_RPC_TAG_0 0x46          /* 'F' */

#define FUBI_RPC_TAG_1 0x52          /* 'R' */

#define FUBI_RPC_TAG_2 0x70          /* 'p' */

#define FUBI_RPC_TAG_3 0x63          /* 'c' */

 

// the full tag

extern const BYTE FUBI_EMBEDDED_RPC_TAG[];

 

// helper for embedded tagging - include initial MOVs to keep the

// disassembler from getting confused. sequence is 0xB8 'Fubi' 0xB8 'abcd'

// in reverse byte order.

#define FUBI_EMBED_TAG( a, b, c, d )                                        \

    {                                                                       \

        __asm jmp $+15                                                      \

        __asm _emit FUBI_EMBED_TAG_MOV                                      \

        __asm _emit FUBI_EMBED_TAG_3                                        \

        __asm _emit FUBI_EMBED_TAG_1                                        \

        __asm _emit FUBI_EMBED_TAG_2                                        \

        __asm _emit FUBI_EMBED_TAG_0                                        \

        __asm _emit FUBI_EMBED_TAG_MOV                                      \

        __asm _emit d                                                       \

        __asm _emit c                                                       \

        __asm _emit b                                                       \

        __asm _emit a                                                       \

    }

 

// helper for "which function am i?" code - puts it into s_FunctionSpec

#define FUBI_FIND_FUNCTION( FUNC_NAME, RESOLVE_PROC, PARAM_SKIP )           \

    /* get the n'th parameter of the function */                            \

    void* paramStart;                                                       \

    {                                                                       \

        /* this is the start of the stack frame, skipping the old ebp */    \

        /* and return addr */                                               \

        __asm mov eax, ebp                                                  \

        __asm add eax, 8                                                    \

        __asm mov paramStart, eax                                           \

    }                                                                       \

    PARAM_SKIP;                                                             \

                                                                            \

    /* get the correct function spec and cache it */                        \

    static const FunctionSpec* s_FunctionSpec = RESOLVE_PROC( GetEIP(),     \

            FUNC_NAME, ELEMENT_COUNT( FUNC_NAME ) - 1 );                    \

    gpassert( s_FunctionSpec != NULL );

 

// special tag to say "this function is an RPC" - 'FRpc'

#define FUBI_RPC_TAG()                                                      \

    FUBI_EMBED_TAG( FUBI_RPC_TAG_0, FUBI_RPC_TAG_1,                         \

                    FUBI_RPC_TAG_2, FUBI_RPC_TAG_3 );                       \

    FuBi::AutoRpcTagBase FUBI_AutoRpcTagBase;                               \

    FuBi::AutoRpcTagBase* FUBI_AutoRpcTagBasePtr = &FUBI_AutoRpcTagBase

 

// implementation for an RPC call

#define FUBI_RPC_CALL_IMPL( FUNC_NAME, THIS_PARAM, RPC_ADDRESS, RETURN )    \

    FUBI_EMBED_TAG( FUBI_RPC_TAG_0, FUBI_RPC_TAG_1,                         \

                    FUBI_RPC_TAG_2, FUBI_RPC_TAG_3 );                       \

    FuBi::AutoRpcTag FUBI_AutoRpcTag( FUBI_AutoRpcTagBasePtr );             \

    {                                                                       \

        using namespace FuBi;                                               \

                                                                            \

        /* get function spec */                                             \

        FUBI_FIND_FUNCTION( FUNC_NAME, ResolveRpc, FUBI_NO_PARAM_SKIP );    \

                                                                            \

        /* send RPC */                                                      \

        DWORD rpcAddress = RPC_ADDRESS;                                     \

        if ( FuBi::RpcTestMacro( rpcAddress, s_FunctionSpec ) )             \

        {                                                                   \

            Cookie cookie = RPC_FAILURE;                                    \

            if ( s_FunctionSpec != NULL )                                   \

            {                                                               \

                cookie = SendRpc( s_FunctionSpec, (void*)(THIS_PARAM),      \

                                  paramStart, rpcAddress );                 \

            }                                                               \

            if ( rpcAddress != RPC_TO_ALL )                                 \

            {                                                               \

                RETURN;                                                     \

            }                                                               \

        }                                                                   \

    }

 

// return helpers

#define FUBI_RPC_RETURN \

    gpassert( cookie == RPC_SUCCESS ); return

#define FUBI_RPC_RETRY_RETURN \

    return ( cookie )

 

// non-retrying RPC calls

#define FUBI_RPC_CALL( FUNC_NAME, RPC_ADDRESS ) \

    FUBI_RPC_CALL_IMPL( #FUNC_NAME, NULL, RPC_ADDRESS, FUBI_RPC_RETURN )

#define FUBI_RPC_THIS_CALL( FUNC_NAME, RPC_ADDRESS ) \

    FUBI_RPC_CALL_IMPL( #FUNC_NAME, this, RPC_ADDRESS, FUBI_RPC_RETURN )

 

// retrying RPC calls

#define FUBI_RPC_CALL_RETRY( FUNC_NAME, RPC_ADDRESS ) \

    FUBI_RPC_CALL_IMPL( #FUNC_NAME, NULL, RPC_ADDRESS, FUBI_RPC_RETRY_RETURN )

#define FUBI_RPC_THIS_CALL_RETRY( FUNC_NAME, RPC_ADDRESS ) \

    FUBI_RPC_CALL_IMPL( #FUNC_NAME, this, RPC_ADDRESS, FUBI_RPC_RETRY_RETURN )

Type System

This is the type system for FuBi. First we have the eVarType enum which is used as an ID for every type that FuBi knows about. All the basic C types are included by default in here, along with some basic mappings to common typedefs for convenience. Next we have a set of “special” types that are handled with custom code for scripting and networking. After that is a set of reserved ID’s for enumerated types. It’s convenient to separate these types from other user types because in most cases they can be treated simply as integers, with only occasionally needing to worry about their actual types for string conversions and type matching. And lastly we have a reserved range for all user-defined types. As we import symbols that we don’t recognize, Note that this enum is treated as “open”, and new ID’s will be assigned in FuBi as it detects types.

enum eVarType

{

    VAR_UNKNOWN = -1,

 

    // standard types

 

    SET_BEGIN_ENUM( VAR_, 0 ),

 

    VAR_SCHAR,                      // signed char

    VAR_UCHAR,                      // unsigned char

    VAR_SHORT,                      // short

    VAR_USHORT,                     // unsigned short

    VAR_INT,                        // int

    VAR_UINT,                       // unsigned int

    VAR_INT64,                      // int64

    VAR_UINT64,                     // unsigned int64

    VAR_FLOAT,                      // float

    VAR_DOUBLE,                     // double

    VAR_VOID,                       // void

    VAR_BOOL,                       // bool

 

    SET_END_ENUM( VAR_ ),

 

    // mapped types - update these if we ever move to win64

 

#   ifdef _CHAR_UNSIGNED

    VAR_CHAR       = VAR_UCHAR,     // char

#   else

    VAR_CHAR       = VAR_SCHAR,     // char

#   endif

 

    // ansi/ms

    VAR_SSHORT     = VAR_SHORT,     // signed short

    VAR_SINT       = VAR_INT,       // signed int

    VAR_LONG       = VAR_INT,       // long

    VAR_SLONG      = VAR_LONG,      // signed long

    VAR_ULONG      = VAR_UINT,      // unsigned long

    VAR_INT8       = VAR_SCHAR,     // int8

    VAR_SINT8      = VAR_SCHAR,     // signed int8

    VAR_UINT8      = VAR_UCHAR,     // unsigned int8

    VAR_INT16      = VAR_SSHORT,    // int16

    VAR_SINT16     = VAR_SSHORT,    // signed int16

    VAR_UINT16     = VAR_USHORT,    // unsigned int16

    VAR_INT32      = VAR_SINT,      // int32

    VAR_SINT32     = VAR_SINT,      // signed int32

    VAR_UINT32     = VAR_UINT,      // unsigned int32

    VAR_SINT64     = VAR_INT64,     // signed int64

    VAR_LONGDOUBLE = VAR_DOUBLE,    // long double - same as double in

                                    // 32-bit code (no longer 80 bit)

 

    // windows

    VAR_BYTE       = VAR_UCHAR,     // Win32 "BYTE"

    VAR_WORD       = VAR_USHORT,    // Win32 "WORD"

    VAR_DWORD      = VAR_ULONG,     // Win32 "DWORD"

 

    // these require special processing

 

    SET_BEGIN_ENUM( VAR_SPECIAL_, 0x1000 ),

 

    VAR_MEM_PTR,                    // mem_ptr

    VAR_CONST_MEM_PTR,              // const_mem_ptr

    VAR_STRING,                     // std::string

    VAR_WSTRING,                    // std::wstring

 

    SET_END_ENUM( VAR_SPECIAL_ ),

 

    // enum - these are enums exported from the game

 

    VAR_ENUM = 0x2000,              // map types to these on the fly

 

    // user types - everything else

 

    VAR_USER = 0x3000,              // map types to these on the fly – they

                                    // correspond to class entries in SysExports

 

    // special tags

    VAR_ENUM_END = VAR_USER,

};

 

MAKE_ENUM_MATH_OPERATORS( eVarType );

 

Next we have a VarTypeSpec, which is a bridge class to the save game and object management systems in Dungeon Siege. FuBi creates one of these for a user-defined type if it’s detected to be a POD type. The implementations of these functions are trivial (just setting members).

struct VarTypeSpec

{

    const char*    m_InternalName;      // internal name of type

    const char*    m_ExternalName;      // external name (presented to users)

    int            m_SizeBytes;         // size in bytes

    Trait::eFlags  m_Flags;             // flags for this type

    ToStringProc   m_ToStringProc;      // convert this type to a string

    FromStringProc m_FromStringProc;    // convert this type from a string

    CopyVarProc    m_CopyVarProc;       // make a copy of the type

 

    VarTypeSpec( void{  ::ZeroObject( *this );  }

    VarTypeSpec( const ClassSpec* spec );

    VarTypeSpec( const EnumSpec* spec );

    VarTypeSpec( const char* iname, const char* ename, int size,

                 Trait::eFlags flags, ToStringProc to,

                 FromStringProc from, CopyVarProc copy );

};

 

This next class is used to represent an individual type. If you’ve ever written a compiler, the first thing you’ll notice is how naïve it is. A type that can be const and a pointer and that’s about it? When I originally designed this system, I had intended it to simply distribute network calls and permit script callbacks. Had I known the degree to which the engine would use it, I would have used a better type system capable of handling complex types. Also note that I tended to use structs rather than classes for the types here. Originally they were extremely simple nested structs used and managed exclusively by the export system. Since then they have grown to fully “self-aware” types, and should have had their members protected from direct access. This would have saved me some trouble when I made updates to the meanings of these variables. FuBi v2 will fix these oversights.

struct TypeSpec

{

    enum eFlags

    {

        FLAG_NONE            =      0,

        FLAG_CONST           = 1 << 0,      // const variable

        FLAG_POINTER         = 1 << 1,      // pointer-to-type

        FLAG_POINTER_POINTER = 1 << 2,      // pointer-to-pointer-to-type (special)         FLAG_REFERENCE       = 1 << 3,      // reference-to-type

        FLAG_HANDLE          = 1 << 4,      // ResHandle <type>

    };

 

    enum eCompare

    {

        COMPARE_EQUAL,                  // types are exactly equal

        COMPARE_PROMOTABLE,             // types can be promoted to match

        COMPARE_COMPATIBLE,             // types can be converted to match

        COMPARE_INCOMPATIBLE,           // not compatible without explicit cast

    };

 

    eVarType m_Type;

    eFlags   m_Flags;

 

    TypeSpec( void )

        : m_Type( VAR_UNKNOWN ), m_Flags( FLAG_NONE )  {  }

    TypeSpec( eVarType type )

        : m_Type( type ),        m_Flags( FLAG_NONE )  {  }

    TypeSpec( eVarType type, eFlags flags )

        : m_Type( type ),        m_Flags( flags )  {  }

 

    bool operator == ( eVarType type ) const

        {  return ( (m_Type == type) && (m_Flags == FLAG_NONE) );  }

    bool operator != ( eVarType type ) const

        {  return ( (m_Type != type) || (m_Flags != FLAG_NONE) );  }

    bool operator == ( TypeSpec type ) const

        {  return ( (m_Type == type.m_Type) && (m_Flags == type.m_Flags) );  }

    bool operator != ( TypeSpec type ) const

        {  return ( (m_Type != type.m_Type) || (m_Flags != type.m_Flags) );  }

 

    bool     IsSimple         ( void ) const;

    bool     IsSimpleInteger  ( void ) const;

    bool     IsSimpleFloat    ( void ) const;

    bool     IsPointerClass   ( void ) const;

    bool     IsPassByValue    ( void ) const;

    bool     IsCString        ( void ) const;

    bool     IsCStringW       ( void ) const;

    bool     IsString         ( void ) const;

    bool     IsStringW        ( void ) const;

    bool     IsAString        ( void ) const;

    bool     IsAStringW       ( void ) const;

    void     SetCString       ( void );

    void     SetString        ( void );

    bool     IsFuBiCookie     ( void ) const;

    bool     IsSingleton      ( void ) const;

    bool     ContentsAreSimple( void ) const;

    bool     CanRPC           ( DEBUG_ONLY( gpstring* cantRpcReason ) ) const;

    bool     CanSkrit         ( void ) const;

    int      GetSizeBytes     ( void ) const;

    TypeSpec GetBaseType      ( void ) const;

};

A TypeSpec is simply a dumb type container. Many times we’ll want to attach some extra information to a type, such as a name, default value, and possible constraints. This new type is called a ParamSpec. Note that I don’t bother to show the constraint system here, because it never really got used for much (time constraints again) and didn’t get a chance for the design to be really tested well. Also note that I use elements of ReportSys here – it’s too large a system to include in this doc but it’s nothing special, just your standard boring stream abstraction (iostreams did not meet my needs).

struct ParamSpec

{

    struct Extra

    {

        gpstring           m_Name;            // name of parameter

        gpstring*          m_DefaultValue;    // def value to use (NULL if none)

        bool               m_DefaultIsCode;   // true if def value is actually code

        bool               m_NoXfer;          // don't bother persisting this

#       if !GP_RETAIL

        my ConstraintSpec* m_ConstraintSpec;  // constraint func applied to param

#       endif // !GP_RETAIL

 

        Extra( void );

       ~Extra( void );

 

        Extra& operator = ( const Extra& other );

    };

 

    TypeSpec m_Type;

    Extra*   m_Extra;

 

    ParamSpec( const ParamSpec& other );

    ParamSpec( const TypeSpec& type, const char* name );

    ParamSpec( const TypeSpec& type )

        : m_Type( type ), m_Extra( NULL )  {  }

   ~ParamSpec( void )

        {  delete ( m_Extra );  }

 

    ParamSpec& operator = ( const ParamSpec& other );

 

    bool operator == ( TypeSpec type ) const

        {  return ( m_Type == type );  }

 

    // extra mod

    Extra* GetExtra( void )

        {  BuildExtra();  return ( m_Extra );  }

    void BuildExtra( void )

        {  if ( m_Extra == NULL )  {  m_Extra = new Extra;  }  }

 

    // attributes

    void SetName( const gpstring& name )

        {  GetExtra()->m_Name = name;  }

    const gpstring& GetName( void ) const

        {  gpassert( m_Extra != NULL );  return ( m_Extra->m_Name );  }

    gpstring GetDefaultValue  ( void ) const;

    void SetDefaultValue( const gpstring& defValue, bool isCode = false );

    void SetNoDefaultValue( void );

 

    // utility

    DWORD CalcDigest( void ) const;

 

    // constraints

#   if !GP_RETAIL

    void                  SetConstraint( ConstraintSpec* spec );

    const ConstraintSpec* GetConstraint( void ) const;

    void                  GenerateDocs ( ReportSys::ContextRef context = NULL,

                                         eDocsLevel level = DOCS_NORMAL ) const;

#   endif // !GP_RETAIL

};

 

Now we’re getting to the really serious stuff. Here is the FunctionSpec class, which contains all the information necessary to type-check and call an exported function.

struct FunctionSpec

{

    enum eFlags

    {

        FLAG_NONE              =       0,

 

        // $ keep these in sync with FunctionSpecFlags in FuBiDefs.h

 

        // call types

        FLAG_CALL_CDECL        = 1 <<  0,   // __cdecl

        FLAG_CALL_FASTCALL     = 1 <<  1,   // __fastcall

        FLAG_CALL_STDCALL      = 1 <<  2,   // __stdcall

        FLAG_CALL_THISCALL     = 1 <<  3,   // __thiscall

        FLAG_CALL_MASK         =     0xF,   // use to mask call convention

 

        // function traits

        FLAG_MEMBER            = 1 <<  4,   // member function of a class

        FLAG_STATIC_MEMBER     = 1 <<  5,   // static member function of a class

        FLAG_CONST             = 1 <<  6,   // member function is const

        FLAG_VARARG            = 1 <<  7,   // variable arg function

 

        // permissions

        FLAG_HIDDEN            = 1 <<  8,   // hidden from ordinary queries

        FLAG_DOCO              = 1 <<  9,   // doco function - not critical

        FLAG_CHECK_SERVER_ONLY = 1 << 10,   // only can execute on server

        FLAG_DEV_ONLY          = 1 << 11,   // only allow this in dev builds

        FLAG_RETRY             = 1 << 12,   // this is a "retryable" function

 

        // misc

        FLAG_POSTPROCESSED     = 1 << 13,   // already postproc’d for doco etc.

        FLAG_SIMPLE_ARGS       = 1 << 14,   // args do not contain ptrs etc.

        FLAG_CAN_RPC           = 1 << 15,   // can be called via RPC

        FLAG_CAN_SKRIT         = 1 << 16,   // can be called from Skrit

        FLAG_SKRIT_IMPORT      = 1 << 17,   // meant for skrit importing

        FLAG_EVENT             = 1 << 18,   // this is a skrit event

        FLAG_TRAIT             = 1 << 19,   // trait-related function

 

        // membership

        FLAG_MEMBER_OF_ALL     = 1 << 20,   // enabled for everybody

        FLAG_MEMBER_OF_GAME    = 1 << 21,   // just enabled for game

        FLAG_MEMBER_OF_CONSOLE = 1 << 22,   // just enabled for the dev console

        FLAG_MEMBER_OF_EDITOR  = 1 << 23,   // just enabled for SiegeEditor

        FLAG_MEMBER_OF_TRIAL   = 1 << 24,   // trial function, don’t rely on it yet

    };

 

    static eFlags ToFlags( FunctionSpecFlags::ePermissions bit );

    static eFlags ToFlags( FunctionSpecFlags::eMembership  bit );

    static bool   TestMembership( eFlags flags,

                                  FunctionSpecFlags::eMembership bit );

 

    static const eFlags DEFAULT_MEMBERSHIP;

 

    typedef std::vector <ParamSpec> ParamSpecs;

 

    gpstring    m_Name;                 // name of this function

    ClassSpec*  m_Parent;               // member of a class, or NULL if global

    eFlags      m_Flags;                // calling convention, modifiers, traits

    DWORD       m_FunctionPtr;          // memory address of start of function

    UINT        m_SerialID;             // serial number based on EXE import order

    ParamSpecs  m_ParamSpecs;           // param types and names this func takes

    UINT        m_ParamSizeBytes;       // total size of parameter set, or if

                                        // vararg then it's size of known params

    TypeSpec    m_ReturnType;           // return type of function

    DWORD       m_Digest;               // digest of this func - use as a checksum

 

#   if !GP_RETAIL

    const char* m_Docs;                 // docs for this function

    const char* m_MangledName;          // original mangled name of the function

    gpstring    m_UnmangledName;        // after we unmangle it - ready to parse

#   endif // !GP_RETAIL

 

    FunctionSpec( void );

 

    bool     IsValidGet                 ( void ) const;

    bool     IsValidSet                 ( void ) const;

    void     PostProcess                ( void );

    gpstring BuildQualifiedName         ( void ) const;

    gpstring BuildQualifiedNameAndParams( const void* param, const void* object,

                                          bool isRpcPacked,

                                          bool allowBinaryOut = false ) const;

 

#   if !GP_RETAIL

    void AssertValid ( void ) const;

    void GenerateDocs( ReportSys::ContextRef context = NULL,

                       eDocsLevel level = DOCS_NORMAL ) const;

#   endif // !GP_RETAIL

};

 

MAKE_ENUM_BIT_OPERATORS( FunctionSpec::eFlags );

 

Most functions in Dungeon Siege are member functions or enclosed in a namespace, and so they end up being owned by a class, which is represented by a ClassSpec:

struct ClassSpec

{

    // flags

    enum eFlags

    {

        FLAG_NONE          =      0,

        FLAG_MANAGED       = 1 << 0,   // this is a "managed" ResHandleMgr class

        FLAG_SINGLETON     = 1 << 1,   // "singleton" class - derive from Singleton

        FLAG_CANRPC        = 1 << 2,   // class ptrs can be sent over the net

        FLAG_PROPCANSKRIT  = 1 << 3,   // my properties are available to Skrit

        FLAG_POSTPROCESSED = 1 << 4,   // already postprocessed for doco, vars…

        FLAG_HIDDEN        = 1 << 5,   // this class is just a placeholder, ignore

        FLAG_HAS_STATICS   = 1 << 6,   // this class has visible static methods or

                                       // it's a namespace with global functions

        FLAG_POD           = 1 << 7,   // plain old data

        FLAG_POINTERCLASS  = 1 << 8,   // pointer class - probably a handle type,

                                       // can never be instantiated

    };

 

    // collections

    typedef std::map <gpstring, VariableSpec, istring_less> VariableMap;

    typedef std::pair <VariableMap::iterator, bool> VariableMapInsertRet;

    typedef std::pair <const ClassSpec*, int> ParentSpec;

    typedef std::vector <ParentSpec> ParentColl;

 

    // procedures

    typedef bool  (__cdecl *DoesHandleMgrExistProc     )( void  );

    typedef UINT  (__cdecl *HandleAddRefProc           )( DWORD );

    typedef UINT  (__cdecl *HandleReleaseProc          )( DWORD );

    typedef bool  (__cdecl *HandleIsValidProc          )( DWORD );

    typedef void* (__cdecl *HandleGetProc              )( DWORD );

    typedef void* (__cdecl *GetClassSingletonProc      )( void  );

    typedef DWORD (__cdecl *InstanceToNetProc          )( void* );

    typedef void* (__cdecl *NetToInstanceProc          )( DWORD, FuBiCookie* );

    typedef void  (__cdecl *GetHeaderSpecProc          )( ClassHeaderSpec& );

 

    // spec

    gpstring               m_Name;                      // class name

    eVarType               m_Type;                      // VAR_USER-based index

    eFlags                 m_Flags;                     // flags for this class

    FunctionSpec::eFlags   m_Membership;                // membership flags

    ClassHeaderSpec*       m_HeaderSpec;                // table spec for schema

    VarTypeSpec*           m_VarTypeSpec;               // traits

    ParentColl             m_ParentClasses;             // derived from what?

 

#   if !GP_RETAIL

    const char*            m_Docs;                      // doco for entire class

#   endif // !GP_RETAIL

 

    // members

    FunctionByNameIndex    m_Functions;                 // static/nonstatic methods

    VariableMap            m_Variables;                 // set/get pairs

 

    // callbacks

    DoesHandleMgrExistProc m_DoesHandleMgrExistProc;    // check for mgr existence

    HandleAddRefProc       m_HandleAddRefProc;          // add ref to a handle

    HandleReleaseProc      m_HandleReleaseProc;         // remove ref from a handle

    HandleIsValidProc      m_HandleIsValidProc;         // check valid handle

    HandleGetProc          m_HandleGetProc;             // deref a handle

    GetClassSingletonProc  m_GetClassSingletonProc;     // look up the singleton

    InstanceToNetProc      m_InstanceToNetProc;         // convert ptr->net cookie

    NetToInstanceProc      m_NetToInstanceProc;         // convert net cookie->ptr

 

    ClassSpec( void );

    ClassSpec( const ClassSpec& other );

   ~ClassSpec( void );

 

    ClassSpec& operator = ( const ClassSpec& other );

 

    bool IsDerivedFrom       ( eVarType type ) const;

    int  GetDerivedBaseOffset( eVarType base ) const;

    void SetPod              ( size_t size );

 

    bool  AddMemberFunction( FunctionSpec* function );

    void  PostProcess      ( void );

    DWORD AddExtraDigest   ( DWORD digest ) const;

 

#   if !GP_RETAIL

    void AssertValid ( void ) const;

    void GenerateDocs( ReportSys::ContextRef context = NULL ) const;

#   endif // !GP_RETAIL

 

private:

    // special: will auto-create spec but only on internal class request

    VarTypeSpec* GetVarTypeSpec( void ) const;

};

 

MAKE_ENUM_BIT_OPERATORS( ClassSpec::eFlags );

 

Finally, this is the big nasty type manager class. The name SysExports comes from FuBi’s original purpose, which was a simple system function binder, or an advanced version of Gabriel Knight 3’s SysExports/Generics system. A lot has changed since then, but the name remains, oh well. It’s also a big monolithic class, which I tend to like, but this one is just too big, which I don’t tend to like. Well anyway here it is:

class SysExports : public Singleton <SysExports>

{

public:

    SET_NO_INHERITED( SysExports );

 

// Types.

 

    // update this as sysexports changes seriously

    enum  {  VERSION = MAKEVERSION( 1, 0, 0};

 

    enum eOptions

    {

        // disable the "found doco for nonexistent export" warning

        OPTION_NO_EXTRA_DOC_WARNING     = 1 << 0,

        // allow rpc's to be used without requiring a sync call

        OPTION_NO_REQUIRE_SYNC          = 1 << 1,

        // don't check for all-all nesting in sent rpc's

        OPTION_NO_CHECK_ALL_RPC_NESTING = 1 << 2,

        // relax certain super-strict warnings

        OPTION_NO_STRICT                = 1 << 3,  

 

        // ...

 

        OPTION_NONE = 0,

    };

 

    struct SyncSpec

    {

        DWORD m_Size;                               // size of this structure

        DWORD m_Version;

        DWORD m_Digest;

 

        SyncSpec( void )

        {

            ::ZeroObject( *this );

            m_Size = sizeof( *this );

        }

 

        SyncSpec( DWORD digest )

        {

            m_Size    = sizeof( *this );

            m_Version = VERSION;

            m_Digest  = digest;

        }

 

        bool operator == ( const SyncSpec& other ) const

        {

            return ( ::memcmp( this, &other, sizeof( *this ) ) == 0 );

        }

    };

 

// Setup.

 

    // ctor/dtor

    SysExports( void );

   ~SysExports( void );

 

    // local options

    void SetOptions( eOptions options, bool set = true )

        {  m_Options = (eOptions)(set ? (m_Options | options)

                                      : (m_Options & ~options));  }

    void ClearOptions( eOptions options )

        {  SetOptions( options, false );  }

    void ToggleOptions( eOptions options )

        {  m_Options = (eOptions)(m_Options ^ options);  }

    bool TestOptions( eOptions options ) const

        {  return ( (m_Options & options) != 0 );  }

 

    // synchronizing for rpc's

    bool Synchronize( const SyncSpec& spec );

    void SetSynchronize( bool set = true )

        {  m_RpcSyncComplete = set;  }

    void ClearSynchronize( void )

        {  SetSynchronize( false );  }

 

    // spec building

    SyncSpec MakeSynchronizeSpec( void ) const;

 

    // callback registration

    void RegisterSendRpcVerifyCb( SendRpcVerifyCb cb )

        {  m_SendRpcVerifyCb = cb;  }

    void RegisterSendRpcBroadcastCb( SendRpcBroadcastCb cb )

        {  m_SendRpcBroadcastCb = cb;  }

    void RegisterDispatchRpcVerifyCb( DispatchRpcVerifyCb cb )

        {  m_DispatchRpcVerifyCb = cb;  }

 

    // exception list for nested rpc warning

#   if GP_DEBUG

    void RegisterNestedRpcException( const char* caller, const char* called );

#   endif // GP_DEBUG

 

// Importing.

 

    bool ImportFunction( const char* mangledName, DWORD address,

                         SignatureParser* parser );

    bool ImportBindings( HMODULE module = NULL );

 

// Type info.

 

    const FunctionSpec*  FindFunctionByAddrExact(

        DWORD ptr, const char* funcName, int funcNameLen ) const;

    const FunctionSpec*  FindFunctionByAddrNear(

        DWORD ptr, const char* funcName, int funcNameLen ) const;

    const FunctionSpec*  FindFunctionBySerialID(

        UINT serialID ) const;

    const FunctionIndex* FindFunctionsByQualifiedName(

        const char* funcName ) const;

    int                  FindFunctionsByQualifiedName(

        FunctionIndexColl& coll, const char* funcName ) const;

    UINT                 FindEventSerialByName(

        const char* eventName ) const;

 

    int GetFunctionSize( const FunctionSpec* spec );

    int GetFunctionCount( void ) const

        {  return ( scast <int> ( m_FunctionsBySerialID.size() ) );  }

 

    const char* FindTypeName      ( eVarType type ) const;

    const char* FindNiceTypeName  ( const FuBi::TypeSpec& spec ) const;

    gpstring    MakeFullTypeName  ( const FuBi::TypeSpec& spec ) const;

    eVarType    FindType          ( const char* name ) const;

    eVarType    FindOrCreateType  ( const char* name );

    eVarType    FindTypeInternalOk( const char* name ) const;

 

    ClassSpec*       GetClass ( const gpstring& name );

    const ClassSpec* FindClass( eVarType type ) const;

    ClassSpec*       FindClass( eVarType type );

    const ClassSpec* FindClass( const char* name ) const;

 

    const ClassSpec* FindEventNamespace( void ) const;

    const ClassSpec* FindSkritObject   ( void ) const;

    const ClassSpec* FindFuBiCookie    ( void ) const;

 

    EnumSpec*       GetEnum         ( const gpstring& name );

    const EnumSpec* FindEnum        ( eVarType type ) const;

    const EnumSpec* FindEnumConstant( const char* name, DWORD& value,

                                      bool fastOnly = false ) const;

 

    const VarTypeSpec* GetVarTypeSpec     ( eVarType type ) const;

    int                GetVarTypeSizeBytes( eVarType type ) const;

    bool               IsSimpleInteger    ( eVarType type ) const;

    bool               IsSimpleFloat      ( eVarType type ) const;

    bool               IsPod              ( eVarType type ) const;

 

    bool IsKeyword           ( const gpstring& name ) const;

    bool IsDerivative        ( eVarType derived, eVarType base ) const;

    int  GetDerivedBaseOffset( eVarType derived, eVarType base ) const;

 

    bool ToString  ( eVarType type, string& out, const void* obj,

                     eXfer xfer = XFER_NORMAL,

                     eXMode xmode = XMODE_DEFAULT ) const;

    bool ToString  ( const TypeSpec& type, gpstring& out, const void* obj,

                     eXfer xfer = XFER_NORMAL,

                     eXMode xmode = XMODE_DEFAULT ) const;

    bool FromString( eVarType type, const char* str, void* obj,

                     eXMode xmode = XMODE_DEFAULT ) const;

    bool FromString( const TypeSpec& type, const char* str, void* obj,

                     eXMode xmode = XMODE_DEFAULT ) const;

    bool CopyVar   ( eVarType type, void* dst, const void* src,

                     eXMode xmode = XMODE_DEFAULT ) const;

    bool CopyVar   ( const TypeSpec& type, void* dst, const void* src,

                     eXMode xmode = XMODE_DEFAULT ) const;

    int  CompareVar( eVarType type, const void* l, const void* r ) const;

    int  CompareVar( const TypeSpec& type, const void* l, const void* r ) const;

 

// Iterators.

 

    ClassIndex::const_iterator GetClassBegin( void ) const

        {  return ( m_ClassVarTypeIndex.begin() );  }

    ClassIndex::const_iterator GetClassEnd( void ) const

        {  return ( m_ClassVarTypeIndex.end() );  }

 

    EnumIndex::const_iterator GetEnumBegin( void ) const

        {  return ( m_EnumVarTypeIndex.begin() );  }

    EnumIndex::const_iterator GetEnumEnd( void ) const

        {  return ( m_EnumVarTypeIndex.end() );  }

 

    FunctionByNameIndex::const_iterator GetGlobalsBegin( void ) const

        {  return ( m_GlobalFunctions.begin() );  }

    FunctionByNameIndex::const_iterator GetGlobalsEnd( void ) const

        {  return ( m_GlobalFunctions.end() );  }

 

// Global functions.

 

    inline const FunctionIndex* FindGlobalFunction( const char* name ) const;

 

// Remote procedure call methods.

 

    const FunctionSpec* ResolveRpc     ( DWORD EIP, const char* funcName,

                                         int funcNameLen ) const;

    Cookie              SendRpc        ( const FunctionSpec* spec, void* thisParam,

                                         const void* firstParam, DWORD toAddr );

    Cookie              DispatchNextRpc( DWORD callerAddr );

    bool                RpcTestMacro   ( DWORD addr, const FunctionSpec* spec );

    bool                IsDispatching  ( void ) const

                                         {  return ( ms_IsDispatching );  }

    bool                ClearDispatcher( void );

    void                EnterRpc       ( const bool* oldDispatching = NULL );

    void                LeaveRpc       ( void );

 

// Callback methods.

 

    const FunctionSpec* ResolveEvent( DWORD EIP, const char* funcName,

                                      int funcNameLen ) const;

    const FunctionSpec* ResolveCall ( DWORD EIP, const char* funcName,

                                      int funcNameLen ) const;

 

// Misc.

 

    DWORD GetDigest( void ) const;

 

#   if !GP_RETAIL

    void GenerateDocs( ReportSys::ContextRef context = NULL ) const;

#   endif // !GP_RETAIL

 

// Debugging.

 

#   if !GP_RETAIL

    void DumpStatistics( ReportSys::ContextRef ctx ) const;

    void DumpStatistics( void ) const  {  DumpStatistics( NULL );  }

#   endif // !GP_RETAIL

 

#   if GP_DEBUG

    void CheckSanity( void ) const;

#   endif // GP_DEBUG

 

private:

 

// Maps.

 

    // these are containers that own their contents

 

    // $ note: lesson learned after hours of debugging - the release linker will

    //   fold identical functions together under the same entry point, while

    //   giving each a separate export entry. this means that many functions

    //   will share the same address! so the FunctionByAddrMap must be multimap.

 

    typedef std::map    <gpstring, ClassSpec*, istring_less>        ClassByNameMap;

    typedef std::map    <const char*, EnumSpec*, istring_less>    EnumByNameMap;

    typedef std::pair   <eVarType, DWORD>                         EnumConstant;

    typedef std::map    <const char*, EnumConstant, istring_less> StringToEnumMap;

    typedef std::multimap <DWORD, FunctionSpec>            FunctionByAddrMap;

    typedef std::pair   <ClassByNameMap::iterator, bool>   ClassByNameMapInsertRet;

    typedef std::pair   <EnumByNameMap::iterator, bool>    EnumByNameMapInsertRet;

    typedef std::set    <gpstring, istring_less>                  StringSet;

    typedef std::vector <VarTypeSpec>                             VarTypeSpecColl;

 

#   if GP_DEBUG

    typedef std::pair     <UINT /*caller*/, UINT /*called*/>  SerialIdPair;

    typedef std::vector   <SerialIdPair>                      SerialIdPairColl;

    typedef std::vector   <UINT>                              SerialIdColl;

#   endif // GP_DEBUG

 

    const FunctionSpec* FindFunctionByAddrHelper( const char* funcName,

        int funcNameLen, FunctionByAddrMap::const_iterator found ) const;

    RpcVerifyColl&      GetRpcVerifyColl( void );

 

    ClassByNameMap    m_Classes;                    // all classes

    EnumByNameMap     m_Enums;                      // all enums

    FunctionByAddrMap m_Functions;                  // all functions

 

// Indexes.

 

    // these are containers that point to elements of maps

 

    ClassIndex          m_ClassVarTypeIndex;        // map eVarType - VAR_USER

                                                    // --> const ClassSpec*

    EnumIndex           m_EnumVarTypeIndex;         // map eVarType - VAR_ENUM

                                                    // --> const EnumSpec*

    EnumIndex           m_IrregularEnums;           // exported irregular enums

    StringToEnumMap     m_EnumConstants;            // all exported enum constants

    FunctionByNameIndex m_GlobalFunctions;          // all global functions

    FunctionIndex       m_FunctionsBySerialID;      // index == serial ID

    VarTypeSpecColl     m_VarTypeSpecs;             // built-in type specs

 

#   if GP_DEBUG

    SerialIdPairColl    m_NestedRpcExceptionColl;   // exceptions to nested rpc’s

    SerialIdColl        m_NestedRpcXCallers;        // match on any called

    SerialIdColl        m_NestedRpcXCalleds;        // match on any caller

#   endif // GP_DEBUG

 

// RPC support.

 

    typedef kerneltool::Critical Critical;

    typedef std::list <gpstring> StringVec;

    typedef std::list <gpwstring> WStringVec;

    typedef std::vector <std::pair <DWORD, RpcVerifyColl> > RpcColl;

 

    mutable Critical    m_SendCritical;             // for serializing RPC sends

    StringVec           m_RpcStrings;               // temp store strings from net

    WStringVec          m_WRpcStrings;              // temp store wstrings from net

    RpcColl             m_RpcColl;                  // per-thread map of call stack

    SendRpcVerifyCb     m_SendRpcVerifyCb;          // verify cb: SendRpc()

    SendRpcBroadcastCb  m_SendRpcBroadcastCb;       // rebroadcasting callback

    DispatchRpcVerifyCb m_DispatchRpcVerifyCb;      // verify cb: DispatchNextRpc()

 

// Other.

 

    eOptions  m_Options;           // general options

    StringSet m_Keywords;          // keywords exported for warnings

    DWORD     m_Digest;            // this is a digest of all of FuBi's exports

    bool      m_RenameComplete;    // renaming is complete

    bool      m_ImportComplete;    // import is complete

    bool      m_RpcSyncComplete;   // rpc synching has been done

 

    DECL_THREAD static bool ms_IsDispatching;

 

    SET_NO_COPYING( SysExports );

};

 

MAKE_ENUM_BIT_OPERATORS( SysExports::eOptions );

 

#define gFuBiSysExports FuBi::SysExports::GetSingleton()

 

In order to reduce dependency on SysExports, and remove the need to #include its header file, all of its functions that are called by the macros are wrapped in helper functions that are implemented in a C++ file to reroute to the SysExports singleton. These functions are left out here because all they do is call the equivalent named SysExports function, which isn’t very interesting. Generally you can take FuBi::FuncName() to mean gSysExports.FuncName().

Exports Implementation

The first critical requirement of SysExports is to be able to import the functions that are FEX’d from the executable. In emails sent to me regarding FuBi, getting this to work right seems to be the major headache so here is the (somewhat messy and verbose) implementation. The ImportBindings function iterates over the entries in the export table and passes each along to ImportFunction, which does all the real work.

Note that the SignatureParser is a simple scanner/parser built with MKS Lex & Yacc. All it does is parse the undecorated names and store the results in a FunctionSpec.

bool SysExports :: ImportFunction(

    const char* mangledName, DWORD address, SignatureParser* parser )

{

    // $$$ this entire function should be !GP_RETAIL

 

    gpassert( !m_ImportComplete );

 

    typedef DWORD (WINAPI* UnDecorateSymbolNameProc)(LPCSTR, LPSTR, DWORD, DWORD);

    int mangledLen = ::strlen( mangledName );

 

    // make sure we've got our dll

    static DbgHelpDll s_DbgHelpDll;

    if ( !s_DbgHelpDll.Load() )

    {

        gpfatal( "FuBi: unable to parse export table because no DBGHELP.DLL "

                 "support, app must fatal\n" );

    }

 

// De-mangle the name.

 

    // $ note that we could decode the mangled name directly by writing a

    //   parser specifically for that. however the format is specific to the

    //   compiler and version, plus there are no publicly available docs on

    //   it. rather than reverse engineer the format by exporting every possible

    //   function variant, just let DbgHelp to do the work for us. to keep from

    //   being dependent on a debug support DLL and exposing our functions to

    //   the world through the export table, a postprocessor will strip the

    //   export table and store it in the resources instead.

 

    // de-mangle into readable text for our cheesy little parser

    char unmangledName[ 0x400 ];

    if ( s_DbgHelpDll.UnDecorateSymbolName(

            mangledName,

            unmangledName,

            ELEMENT_COUNT( unmangledName ),

            UNDNAME_COMPLETE | UNDNAME_32_BIT_DECODE ) == 0 )

    {

        return ( false );       // $ error is in ::GetLastError();

    }

 

    // check for special exports that we should ignore

    static const char* s_IgnoreFuncs[] =

    {

        "CoverageAddAnnotation",

        "CoverageClearData",

        "CoverageDisableRecordingData",

        "CoverageIsRecordingData",

        "CoverageIsRunning",

        "CoverageSaveData",

        "CoverageStartRecordingData",

        "CoverageStopRecordingData",

        "PurelockIsRunning",

        "PurelockPrintf",

        "PurePrintf",

        "PurifyAllHandlesInuse",

        "PurifyAllInuse",

        "PurifyAllLeaks",

        "PurifyAssertIsReadable",

        "PurifyAssertIsWritable",

        "PurifyBlue",

        "PurifyClearInuse",

        "PurifyClearLeaks",

        "PurifyDescribe",

        "PurifyGetPoolId",

        "PurifyGetUserData",

        "PurifyGreen",

        "PurifyHeapValidate",

        "PurifyIsInitialized",

        "PurifyIsReadable",

        "PurifyIsRunning",

        "PurifyIsWritable",

        "PurifyMapPool",

        "PurifyMarkAsInitialized",

        "PurifyMarkAsUninitialized",

        "PurifyMarkForNoTrap",

        "PurifyMarkForTrap",

        "PurifyNewHandlesInuse",

        "PurifyNewInuse",

        "PurifyNewLeaks",

        "PurifyPrintf",

        "PurifyRed",

        "PurifySetLateDetectScanCounter",

        "PurifySetLateDetectScanInterval",

        "PurifySetPoolId",

        "PurifySetUserData",

        "PurifyWhatColors",

        "PurifyYellow",

        "QuantifyAddAnnotation",

        "QuantifyClearData",

        "QuantifyDisableRecordingData",

        "QuantifyIsRecordingData",

        "QuantifyIsRunning",

        "QuantifySaveData",

        "QuantifyStartRecordingData",

        "QuantifyStopRecordingData",

    };

 

    // c++ exports always start with ?

    if ( *mangledName != '?' )

    {

        const char** i, ** ibegin = s_IgnoreFuncs, ** iend

            = ARRAY_END( s_IgnoreFuncs );

        for ( i = ibegin ; i != iend ; ++i )

        {

            if ( same_with_case( unmangledName, *i ) )

            {

                return ( true );

            }

        }

    }

 

// Build function spec.

 

    // add generic spec to map

    FunctionByAddrMap::iterator newFunc

        = m_Functions.insert( std::make_pair( address, FunctionSpec() ) );

    FunctionSpec& spec = newFunc->second;

 

    // initialize

    spec.m_FunctionPtr = address;

    spec.m_SerialID    = m_FunctionsBySerialID.size();

 

#   if !GP_RETAIL

    spec.m_MangledName   = mangledName;

    spec.m_UnmangledName = unmangledName;

#   endif // !GP_RETAIL

 

    // add it

    m_FunctionsBySerialID.push_back( &spec );

 

    // this should be SMALL - plus it must be in a word anyway for RPC's

    gpassert( m_FunctionsBySerialID.size() <= std::numeric_limits <WORD