D's built-in TypeInfo
doesn't provide nearly as much information as we can get from the compile-time reflection tools. Here, we'll create an extended type info with custom data.
Let's execute the following steps to extend the runtime type information:
Object
or void*
in the generic interface.TypeInfo
.typeid()
to get the lookup key from a given variable or type.std.traits
Phobos module will help with our implementation.mixin
template that loops over all the types to which you wish to add support for extended type information. Alternatively (or additionally), you could provide a registerType
function to be called explicitly or on request in the extended info lookup functions.The code is as follows:
/// stores the information immutable MoreInfo[TypeInfo] moreTypeInfo; /// Gets extended info from a value public immutable(MoreInfo) extTypeInfo(T)(T t) { if(auto ptr = typeid(t) in moreTypeInfo) return *ptr; return null; } /// Gets extended info from a type public immutable(MoreInfo) extTypeInfo(T)() { if(auto ptr = typeid(T) in moreTypeInfo) return *ptr; return null; } /// The interface we provide to more info public interface MoreInfo { immutable: /// is it an integral type? bool isIntegral(); /// given a pointer to this type, return it as a string /// Be sure the pointer is actually the correct type! string getAsString(in void*); } /// The implementation of the interface for any type private class MoreInfoImpl(T) : MoreInfo { immutable: // std.traits offers reflection helpers import trait = std.traits; bool isIntegral() { return trait.isIntegral!T; } string getAsString(in void* ptr) { import std.conv; auto t = cast(T*) ptr; return to!string(*t); } } /// This creates all the instances we want to enable public mixin template EnableMoreInfo(types...) { /// A static constructor is run at thread startup shared static this() { foreach(type; types) moreTypeInfo[typeid(type)] = new immutable MoreInfoImpl!type(); } } /* User code */ class A { } // a random custom class // enable more info for built-in ints and our class mixin EnableMoreInfo!(int, A); void main() { import std.stdio; // test writeln(extTypeInfo(1).isIntegral()); // true writeln(extTypeInfo!(A).isIntegral()); // false int a = 34; writeln(extTypeInfo(a).getAsString(&a)); // prints 34 }
We used three features to make information from compile-time reflection available at runtime: classes with interfaces, static module constructors, and associative arrays with the TypeInfo
keys.
First, the interfaces were used to provide a consistent base type that always works at runtime. Sometimes, it is tempting to think of templates as being almost magical—if we could only write cast(runtime_type)
it will all work. However, this is impossible because templates and types only exist at compile time. If you had enough information to successfully perform a downcast, you'd have enough information to use compile-time reflection and thus have no need for runtime information anyway!
So, while the class that implements the interface is a template, allowing us to use compile-time reflection to fill in the information, saving a lot of manual work. The interface itself is what must be used at runtime and it must all work with generic types. To get a type's value generically, we put getAsString
in the interface. No matter what the underlying data is, we can convert it to one uniform type, string, and use that. Since the interface itself isn't a member of the type we're converting, getAsString
must take a pointer to the data so it knows what to convert. Given the limitations of the generic MoreInfo interface, the data pointer must also be generic, so we use void*
.
Other interface methods that operate on the data will need to work the same way; return a consistent type and take a generic pointer to the data.
Once we've defined and implemented the interface, we need some way to access it at runtime. That's where the associative array comes in. Using TypeInfo
as a key, we can look up our extended information any time we are given a TypeInfo+void*
pair elsewhere in the code, including generic classes. We can work with classes with a generic base type of Object
instead of void*
if you prefer, and we can always retrieve their TypeInfo
with the typeid
operator, but that only works on class objects. Remember, class objects are unique and they always carry their TypeInfo
reference with the actual data.
However, how do we populate the associative array with data coming from several sources, possibly across modules and compilation units? That's where the static module constructor comes in.
A static module constructor is declared with one of two syntaxes: static this()
or shared static this()
, both in module scope. The static this()
constructor declares a thread-local module constructor. Each new thread spawned will run its code to initialize thread variables. The shared static this()
constructor declares a module constructor for the data shared across all threads, including the immutable data. A shared static
constructor is run only once, automatically upon program start-up, before main
is run.
You can also write static constructors for your classes and complex types by using the same syntax in the object's scope. It is important that you write the definition exactly as it is seen here; the literal keywords static this
or shared static this
must appear, in that order, for the compiler to recognize the constructor.
Since our extended type info never changes, we made it immutable. Immutable data normally cannot be modified, but the compiler makes an exception for constructors to allow them to be initialized. Since it is immutable, it can also be shared across threads, so we initialized it in a shared static
constructor. Thus, the cost of initialization is minimized because it is done once and only once.
What happens if you have multiple calls to EnableTypeInfo
in the same module? Try it, you'll find it works. The compiler will combine multiple module constructors together automatically.
Once the array is built, we can use it like any other collection of data. The extTypeInfo
helper functions give us an easy interface to use a given type or variable, or we can access the array directly with a TypeInfo
key. Since our extended type information is opt-in, these functions may return a null
value, indicating that the extended information is not available for the given type. Here, we used a small manually curated list of types that will be enabled. We could have also used compile-time reflection to search the module for all types it defines.
Why don't we construct the extended information lazily in the helper function? Immutability of the array notwithstanding, that will actually work for some types. The problem is lazy construction only works where compile-time reflection will work anyway, defeating the point of enabling runtime reflection. An example where it will not work correctly is when you give it an instance of a child class through a base class. The lazy function will construct information for the base class, unaware of the child class.
The D runtime and compiler also includes a feature we could use to make extended type information for all user-defined types automatically. In the documentation of TypeInfo
at dlang.org, you might have noticed a member rtInfo
, which provides implementation-specific type extensions for the garbage collector.
Every time the compiler encounters a new type definition, it invokes a template called RTInfo
on that type. The RTInfo
template is found in the automatically imported core library file, object.d
. The RTInfo
template can return a pointer to the implementation-defined data that is accessible through TypeInfo
, and it can also define new module constructors to build additional lists, exactly as we did here.
The same feature could, in theory, be used for user-defined type extensions too. However, since customizing it means customizing your core runtime library, it isn't very practical today. There's ongoing work in the D community to make this feature more accessible to end users.
18.219.63.95