MetaData.hpp - Header for the C++ introspection class
So what does this class do?
- This class stores basic runtime type information about a C++ type (built-in or user-defined). Information is gathered at compile time (when the compiler knows all there is to know about a type) and is stored for use at runtime. This works with template meta-programming, where I have the C++ compiler instantiate functions that perform the actions I'm requesting (like copy constructing a class). There is also some game specific information stored here (IsSpace(), IsComponent(), etc). Basically, this class stores any information that can be useful for python or level editors to use.
In this class you can see a few things about my coding style:
1) I like to organize code in namespaces (or modules for languages that have them)
2) I generally wrap access of a class's components inside of the interface to that class
Why do I do those things?
- I use namespaces in C++ mostly for the auto-completion. :) It makes it easy for a dev using my code to get a list of all of the available methods/types pertaining to a particular object. Ideally, I only have data and operators on the object and all of it's methods are stored in a related namespace, C-style. This makes it easier to parallelize code and stick to only public interfaces.
- I generally wrap any call through code inside of the class interface (so meta->prop_holder->members becomes meta->GetMemberProperties()) because this allows me to change how access to the class's internals work without the user having to change their code. In general, I think that's good API design.
What could I have done better?
- I could have wrapped the internals of this class into either a pimpl or made the metaclass an interface. Then the internals of the class wouldn't have been visible outside of the class. The reason I didn't do this was because it would have made some initialization logic even more complicated (since I derive another class from MetaData). I could also argue that the extra indirection could have been costly since this class is ubiquitous in our engine, but without profiling, I'd honestly just be talking out of my... yep.
- I could have made a bit flag for all of the bool variables. This would drastically cut down on the size of the MetaData object. It honestly wouldn't have been hard to make this memory optimization, with an enum and a macro/function the code would've practically written itself, but I knew that there was only a limited number of MetaData objects that could exist in our engine (one for every C++ type) and there were other things to write.
- I could've made it so that meta only deals with public information and separated serialization (which probably needs access to private data) into a separate function. This would've allowed meta to not have to be a member a friend of the object I was binding and the GetProp/GetFunc/GetData() functions would have been a lot safer.
- This class stores basic runtime type information about a C++ type (built-in or user-defined). Information is gathered at compile time (when the compiler knows all there is to know about a type) and is stored for use at runtime. This works with template meta-programming, where I have the C++ compiler instantiate functions that perform the actions I'm requesting (like copy constructing a class). There is also some game specific information stored here (IsSpace(), IsComponent(), etc). Basically, this class stores any information that can be useful for python or level editors to use.
In this class you can see a few things about my coding style:
1) I like to organize code in namespaces (or modules for languages that have them)
2) I generally wrap access of a class's components inside of the interface to that class
Why do I do those things?
- I use namespaces in C++ mostly for the auto-completion. :) It makes it easy for a dev using my code to get a list of all of the available methods/types pertaining to a particular object. Ideally, I only have data and operators on the object and all of it's methods are stored in a related namespace, C-style. This makes it easier to parallelize code and stick to only public interfaces.
- I generally wrap any call through code inside of the class interface (so meta->prop_holder->members becomes meta->GetMemberProperties()) because this allows me to change how access to the class's internals work without the user having to change their code. In general, I think that's good API design.
What could I have done better?
- I could have wrapped the internals of this class into either a pimpl or made the metaclass an interface. Then the internals of the class wouldn't have been visible outside of the class. The reason I didn't do this was because it would have made some initialization logic even more complicated (since I derive another class from MetaData). I could also argue that the extra indirection could have been costly since this class is ubiquitous in our engine, but without profiling, I'd honestly just be talking out of my... yep.
- I could have made a bit flag for all of the bool variables. This would drastically cut down on the size of the MetaData object. It honestly wouldn't have been hard to make this memory optimization, with an enum and a macro/function the code would've practically written itself, but I knew that there was only a limited number of MetaData objects that could exist in our engine (one for every C++ type) and there were other things to write.
- I could've made it so that meta only deals with public information and separated serialization (which probably needs access to private data) into a separate function. This would've allowed meta to not have to be a member a friend of the object I was binding and the GetProp/GetFunc/GetData() functions would have been a lot safer.
MetaFunction.hpp/MetaFunctionTest.cpp - C++ function bindings
What's going on here?
- MetaFunction is a class that is used to bind free/static C++ functions and C++ member functions so that they can be invoked uniformly and from a scripting language! The basic idea is that I'll take any function and transform it into a wrapper function that returns an "any" type object, Variant, and takes an array of pointers to "any" type objects, VariantRefs. If you think about it, that's all any function call is, a block of code that takes a variable number of arguments and returns a value; the return value can be "void" and the number of arguments could be "0" but the basic concept is the same. The only issue is that member functions take an extra argument, a pointer to the instance calling the function, so I had to make my wrapper function's signature account for that with an extra argument, the calling object's instance pointer; This pointer is null for free functions.
What would I change?
- Variadic templates would greatly reduce the amount of code I had to copy/paste...ahem...write. Unfortunately, this project was required to be run in vs2010, so that wasn't an option. So I arbitrarily decided to support up to 4 arguments for functions.
- MetaFunction is a class that is used to bind free/static C++ functions and C++ member functions so that they can be invoked uniformly and from a scripting language! The basic idea is that I'll take any function and transform it into a wrapper function that returns an "any" type object, Variant, and takes an array of pointers to "any" type objects, VariantRefs. If you think about it, that's all any function call is, a block of code that takes a variable number of arguments and returns a value; the return value can be "void" and the number of arguments could be "0" but the basic concept is the same. The only issue is that member functions take an extra argument, a pointer to the instance calling the function, so I had to make my wrapper function's signature account for that with an extra argument, the calling object's instance pointer; This pointer is null for free functions.
What would I change?
- Variadic templates would greatly reduce the amount of code I had to copy/paste...ahem...write. Unfortunately, this project was required to be run in vs2010, so that wasn't an option. So I arbitrarily decided to support up to 4 arguments for functions.
Are these tests?! Tests?! O_o
- Yes. I find that testing my code allows me to refactor, freely and quickly. I also seem to make less mistakes and find bugs faster. So I test all of the code that I write; well, the majority of it.
That's a lot of test code though. Why?
- Since I couldn't use the compiler to generate code for me (as explained above) I had to handwrite a lot of what the compiler could've instantiated for me; this involved a lot of copy and paste so I wanted to make sure that I didn't make any silly typos.
What could you improve?
- I could refactor some of the code in the invocation tests into the test fixture which could get rid of a few lines of code.
- Yes. I find that testing my code allows me to refactor, freely and quickly. I also seem to make less mistakes and find bugs faster. So I test all of the code that I write; well, the majority of it.
That's a lot of test code though. Why?
- Since I couldn't use the compiler to generate code for me (as explained above) I had to handwrite a lot of what the compiler could've instantiated for me; this involved a lot of copy and paste so I wanted to make sure that I didn't make any silly typos.
What could you improve?
- I could refactor some of the code in the invocation tests into the test fixture which could get rid of a few lines of code.
Variant.hpp/VariantTest.cpp - C++ "any" type and it's tests
Is this an "Any" type?
- Yes! Good eye. When converting to a from two different languages (for script-binging or serialization) a lot of the time the actual type of the data doesn't matter. For example, whether I'm reading in an integer or a string from a text file, I still want to do the same thing. I want to read in data from that file into a block of memory I've allocated in my game engine. If the "any" type knows how to read in it's data from a file, then all I have to do is say any_type.ReadYourselfInSon(). This abstraction makes working with types very easy, because they're all the same at a high enough level.
What could you improve?
- For data types less than 4 bytes, I should just store them inside of the Variant instead of having to use a pointer to access the data.
- I really need to clean-up the conversion code in VariantRef. Thanks to Treb Connell for showing me how to do these conversions.
- Yes! Good eye. When converting to a from two different languages (for script-binging or serialization) a lot of the time the actual type of the data doesn't matter. For example, whether I'm reading in an integer or a string from a text file, I still want to do the same thing. I want to read in data from that file into a block of memory I've allocated in my game engine. If the "any" type knows how to read in it's data from a file, then all I have to do is say any_type.ReadYourselfInSon(). This abstraction makes working with types very easy, because they're all the same at a high enough level.
What could you improve?
- For data types less than 4 bytes, I should just store them inside of the Variant instead of having to use a pointer to access the data.
- I really need to clean-up the conversion code in VariantRef. Thanks to Treb Connell for showing me how to do these conversions.
The tests for these two classes are pretty simple. I just test construction and converting between different types.
Premake4.lua - build script for GTFO
This is the build script for GTFO at an early stage in the project's lifecycle. I posted this more as a sample for other students/hobbyist devs to check out. This build script automatically sets up precompiled headers and test executables for project libraries. In order to make it so that the testing framework was automatically included in every test executable, I made an "example" project that a library writer could copy that would have all of the required includes and what not to automagically run gmock.