Reflecting with Coffee (Part 4)

The journey so far ... Part 1 - all about the wonderful things we get with reflection,  Part 2 - what a type introspection runtime might look like and Part 3 - extracting the symbols from our source code with clang.

The final step is to connect the two together and fill in a few details. The reflector code is deliberately simple, the script does all the processing work. It is relatively straightforward and does not really require any explanation. Its job is to the JSON output from reflector and fill in the TypeInfoImpl::Create() specialisation.

Any scripting language would do here. In the past I probably would have reached for Perl but my language de jour is coffeescript. There are various reasons for this but feel free to insert language of choice here :) Preferably one with a handy template engine, I happen to be using doT.js.

There are a few extra bits and bobs I added that maybe worth a mention. For convenience, fundamental types (floats, ints, etc) get "type converters" for converting to and from strings. Nothing particularly exciting.

Container types are more fun. If a class fields is a container (say std::vector) then we need a way to iterate over it when saving or to insert into it when loading it back in. Being abstract metadata type of stuff then we need to provide an abstract runtime interface for this.

This means in the script, we need to identify the container type and write the appropriate iterator and inserter field. I'm using std::functions to wrap up the implementation into an abstract callable interface. 
    
struct GWRTTI_API Field
{
    std::function< std::pair<void*,const TypeInfo*>() > (*Iterator)( void* );
    std::function< bool(void*) >                        (*Inserter)( void*, int );
};

The iterator returns a pointer to the next item along with a type, both are null when we are at the end. So in the case of a std::vector we can implement it with a funky little mutable lambda.
    
fields[ 2 ].Iterator = []( void* o ) -> std::function< std::pair< void*,const TypeInfo* >() >
{
    auto obj = reinterpret_cast<GameObject*>( o );
    auto itr = std::begin( obj->Components );
    auto end = std::end( obj->Components );

    return [=]() mutable -> std::pair< void*,const TypeInfo* >
    {
        if( itr == end ) return std::make_pair( nullptr, nullptr );
        auto cur = *itr++;
        return std::make_pair( cur, cur->GetType() );
    };
}
This slightly obscure bit of code is a function that returns a std::function, which is implemented by the lamda. The lambda holds and can change (hence mutable) the iterators for the container. Gotta love C++11 :)

Inserters are slightly easier. I pass in the size merely so I can pre-allocate the space and the function returned provides the interface for adding to the collection.
fields[ 2 ].Inserter = []( void* o, int size ) -> std::function< bool(void*) >
{
    auto obj = reinterpret_cast<GameObject*>( o );

    if( size > 0 )
    {
        obj->Components.reserve( obj->Components.size() + size );
    }

    return [=]( void* i ) -> bool
    {
        obj->Components.push_back( reinterpret_cast< Component* >( i ) );
        return true;
    };
}
There is no type checking, the assumption is this is done by the serialiser. Of course we would need to implement iterators and inserters for all our collection types but it suffices for the proof of concept code.




Comments

Popular posts from this blog

Game Development in a Post-Agile World

Comments

Polyphasic Sleep - Dymaxion Day 1