Scripting languages offer the comfort of automatic type inference. The way we approached this in our own scripting language
was by having meta-data alongside memory containing the data.

*A few commenters recommended unions. Although interesting, we prefered to extend the enum variables and keep the pointer variable flexible, instead of extending the struct with new types.

Languages that auto-magically infer the type from what you populate
the object with like Javascript, Python and Lua, store meta-data about
your data. This meta-data is used to allow the correct operations to happen
on said objects.

[toc]

On Klep The Thief, we were writing our own types in order to speed up and have
a more flexible game engine. The yester-iPhones were tight on specs and had
small memory buses, so streaming tightly packed data was critical to hit
respectable frame rates.

Here’s a discussion of how we went about writing fundamental technology that helps power our scripting engine.

Game engines require high performance, so short of Assembly language,
most of our core engine, is mostly in C.

Data and Meta-Data

typedef struct
{
    typedef enum
    {
        TYPE_VEC3 = 0,
        TYPE_CCHAR,
        TYPE_LINE3D
    } TYPE;
    
    void *ptr;
    TYPE type;
    
} __attribute__((packed, aligned(1)))  PZE1datatype;

 

Data Alignment In Memory

Structs are the objects of choice as they tightly pack data with minimal
extra information except for an access address and jumps from that address
in order to access the struct’s members. The compiler attribute function is used
to avoid padding as an extra measure of compactness.

struct1

Meta-Data

Digging into the struct, we have a type enum, which is an int and a void pointer.
The void* is used to allow flexibility in allocation and also casting. The
memory void points to memory which will be interpreted using information from the
type enum.

Primitive Allocation and De-Allocation

Initialization

 

PZE1datatype *pze1DataTypeInit( void *ptr, PZE1datatype::TYPE type )
{
    PZE1datatype *_PZE1datatype = (PZE1datatype*)calloc( 1, sizeof( PZE1datatype ) );

    if( _PZE1datatype ) // Successful allocation.
    {
        _PZE1datatype->ptr = ptr;
        _PZE1datatype->type = type;
    }
    
    return _PZE1datatype;
}

The default initializer will first allocate memory. Calloc() is used instead of Malloc() in order to ensure the zero-ing out of memory allocated on the heap. We then fill in the struct with a pointer and type information. Allocation of the ptr variable is delegated in order to give freedom to the client on how to allocate and store the memory. This also allows the PZE1datatype to act as a pointer if need be.

Deallocation:

 

PZE1datatype *pze1DataTypeFree( PZE1datatype *_PZE1datatype )
{
    if( _PZE1datatype )
    {
        if( _PZE1datatype->ptr )
            free( _PZE1datatype->ptr );
        
        free( _PZE1datatype );
        _PZE1datatype = NULL;
    }
    return NULL;
}

During deallocation, we free any memory in the ptr variable. Making sure we set passed in PZE1datatype to a NULL for any subsequent deallocation checks.

Initialization of Types

Vector

 

Rational for delegating allocation of the ptr variable can be understood through usage of the pze1DataTypeInit function. 

PZE1datatype *pze1DataTypeInitVec3( const vec3 *v )
{
    vec3 *Vec3 = (vec3*)calloc( 1, sizeof( vec3 ) );
    memcpy( Vec3, v, sizeof( vec3 ) );
    PZE1datatype *_PZE1datatype = pze1DataTypeInit( Vec3, PZE1datatype::TYPE_VEC3 );
    return _PZE1datatype;
}

For a vec3 initialization, the size of the type is not needed by the pze1DataTypeInit function. Only the client is responsible of the storage. PZE1datatype will strictly take care of storing type information of the memory address passed down to the ptr variable.

Character Arrays (String)

 

PZE1datatype *pze1DataTypeInitCChar( const char *str )
{
    char *Str = (char*)malloc( sizeof( char ) * (pze1StringLen( str ) + 1 ) );
    pze1StringCpy( Str, str );
    PZE1datatype *_PZE1datatype = pze1DataTypeInit( Str, PZE1datatype::TYPE_CCHAR );
    return _PZE1datatype;
}

In this more complex example, PZE1datatype can take an array for the ptr variable. This allows compatibility with strings as well as more complex structures like trees and linked-lists.

Usage

PZE1datatype was extensively used in our custom scripting language. Our scripters needn’t focus on type information, as that was handled by this struct and pertaining helper functions.

// Location
if( _PZE1objects[ i ]->_PZE1datatypeLoc )
{
    switch ( _PZE1objects[ i ]->_PZE1datatypeLoc->type ) 
    {
        case PZE1datatype::TYPE_CCHAR:
            memcpy( _PZE1object->_PZE1transform->loc,
                   pze1ResourceGetObject( pze1->_PZE1resource, (char*)_NAeventCur->_PZE1objects[ i ]->_PZE1datatypeLoc->ptr )->_PZE1transform->loc,
                   sizeof(vec3) );
            break;
        // Handling of more data types
        default:
            break;
    }
}